• Welcome to Jose's Read Only Forum 2023.
 

COM Tutorial #1: COM Object Memory

Started by Frederick J. Harris, January 20, 2009, 08:38:02 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Frederick J. Harris

Com Tutorial #1:  Component Object Model Memory Layout

    I have many books on COM and they all have pictures of the memory layout Microsoft's Component Object Model uses involving virtual function table pointers and the virtual function table itself.  The pictures with their little boxes and arrows pointing about are helpful but left me somewhat dissatisfied that I fully comprehended what exactly was going on with this material.  The problem is further compounded and confused by the fact that all my books use C++ in describing the workings of COM, and that language effectively hides quite a bit of what is really taking place in memory.  I've fought the thing for quite a while and finally developed several programs that for me at least have been quite helpful in seeing what is actually going on.  I will present them shortly in the hope they may be useful to you also.  If you are trying to master COM two books that I have found useful are "Inside COM" by Dale Rogerson, and "Inside DCOM" by Guy and Henry Eddon.  The latter book covers connection points in some detail.  If you attempt to get these books make sure you get the CDs with them as you will need the source code.

    Returning to the topic though, my point concerning the difficulties of comprehending what is going on in memory with COM objects is something like the following.  Let us take a simple integer array that holds five elements, say – five 32 bit PowerBASIC longs.  It really isn't hard to completely and exhaustively describe the exact nature of this data structure in memory.  The compiler must allocate space for five longs and that will require 20 bytes.  It will obtain these 20 bytes and perhaps a few extra for good measure (due to memory allocation granularity) from the operating system, and in the process of doing that will obtain the base address of this memory.  It is into that memory that we can store our five longs and we can access them through subscripts and the symbol we told PowerBASIC we want associated with the array.  We can easily output both the contents of this array and the various memory addresses of the individual elements.  

    Well, what about a COM Object?  Can't we describe it in exactly the same terms?  As a matter of fact we can, but it's one heck of a lot trickier.  Following are some specific issues with COM objects that don't have any real close analogies with simple arrays...

1)   COM objects are created by Api function calls to the operating system, not simple memory allocations;
2)   COM objects involve multiple memory allocations of various sizes that occur in various places in memory – not just in one place;
3)   COM objects consist of loads of pointers, i.e., pointers to the class object, pointers to V Tables, pointers to functions, etc.

    To help accomplish what I wanted to do I thought it best to create my own simple COM object using C++ and use that object in my deconstruction work of finding out where things are at in memory.  If I created a simple COM object with PowerBASIC, I'd be able to call methods through function pointers as I did in a demo on ObjPtr I posted in PowerBASIC Source Code some time ago....

http://www.powerbasic.com/support/pbforums/showthread.php?t=38544

...but I wouldn't be able to fool around with QueryInterface or any of the other unique COM functions because their implementation would have been internal to the PowerBASIC binaries and not accessible by me.  In other words, I don't have access to the source code that created the objects.  So I'll provide C++ source code for a COM object and class I named 'CA' for 'Class A'.  I'll also provide the binary for anyone who wants to register it and run any of my demo code here.  However, I'll show the outputs along with the various programs so that those who do not wish to delve into this very deep will still be able to follow along with my discussion if interested.  For folks that may be really 'into' this sort of thing, I'll provide directions also for compiling the C++ source into a dll.  I personally have Microsoft Visual C++ 6, Microsoft Visual C++ 2008 (VC9), Dev-C++, and CodeBlocks.  Unfortunately, the GNU compilers don't seem to like Microsoft's Component Object Model very much.  I fought long and hard with Dev-C++ to get clean compiles and working dlls but with mixed results.  Sometimes on some computers it can be made to work, and on others it won't.  Pretty much disgusted with the whole thing I only tried CodeBlocks a few times without much success.  So if anyone wants to fight with those development systems I'll try to help, but can't promise success.  The Microsoft compilers work fine though, and Visual C++ 2008 Express can be downloaded for free.

    Now I realize this is a PowerBASIC oriented site so I hope I'm not offending anyone – especially including Jose – by the quantity of C++ code I'm going to post.  However, I wouldn't do this if I didn't feel that there were over-riding benefits to it for PowerBASIC programmers who may, like me, have been confused by the almost mystical nature of COM objects and Apis.  For I feel that to really understand this stuff one must be willing to at least look closely at Microsoft's C & C++ oriented documentation concerning the technology, not to mention in addition that all the books on COM are written for C++ programmers. But PowerBASIC programmers do not dismay!  There will be lots of PowerBASIC code too.  In fact, my greatest personal satisfaction in developing this code I'm going to post occurred when I finally got everything working in PowerBASIC – for it was only then that I felt that I fully understood what was going on.

    So to begin with I'll post an only slightly modified version of the program I mentioned above regarding my ObjPtr() demo in the PowerBASIC forum – the only modification really being some more comments explaining what is going on in the code.  It will essentially be this program that I will be re-building in C++ as a COM dll and accessing with a PowerBASIC client in just a bit....  


'ObjPtr.bas
#Compile Exe                         'While the interface declarations just
#Dim All                             'below for I_X and I_Y can be deleted
Declare Sub pFn(dwPtr As Dword)      'and this program will still work, I...


Interface I_X : Inherit IUnknown     '...thought for clarity sake it might be
 Method Fx1()                       'worthwhile to include them for the purpose
 Method Fx2()                       'of making the point that COM architecture
End Interface                        'is based on the idea of seperating the...


Interface I_Y : Inherit IUnknown     '...interface from the implementation of
 Method Fy1()                       'the interface.  Here, I_X and I_Y are
 Method Fy2()                       'declared - but not implemented.  They are
End Interface                        'implemented in class CA, and the ObjPtr()


Class CA
 Interface I_X : Inherit IUnknown   '...function can be used to obtain the
   Method Fx1()                     'address of each interface's VTbl.  Where
     Print "Called Fx1()"           'PowerBASIC seems to differ somewhat from
   End Method                       'C++ is that in this situation in C++ the
                                    'Sizeof(CA) would be 8 and those 8 bytes
   Method Fx2()                     'would be two contiguous VTable pointers.
     Print "Called Fx2()"           'PowerBASIC will return two interface
   End Method                       'pointers also but they do not appear to
 End Interface                      'be contiguous.  Below the I_X pVTbl is
                                    '1280208 and the I_Y interface pointer is
 Interface I_Y : Inherit IUnknown   'at 1280340 - not 1280212.  Nonetheless
   Method Fy1()                     'they each can be used to obtain the
     Print "Called Fy1()"           'address of each interface's VTable, and
   End Method                       'the function pointers held in the VTables
                                    'can be used to call the interface
   Method Fy2()                     'functions.  This probably isn't really
     Print "Called Fy2()"           'recommended but this exercise at least
   End Method                       'shows the memory layout.
 End Interface
End Class


Function PBMain()                   'When one uses ObjPtr() on a variable of type
 Local pVTbl  As Dword Ptr         'interface one obtains a pointer to the base
 Local VTbl   As Dword Ptr         'address of the interface/vtable.  In the case
 Register i   As Long              'here where each interface only has two functions
 Local ifX    As I_X               'the base address will point to a block of memory
 Local ifY    As I_Y               'occupying 20 bytes - 12 bytes for pointers to
                                   'the three IUnknown functions of QueryInterface(),
 Let ifX = Class "CA"              'AddRef(), and Release(), and 8 more bytes for
 Let ifY = Class "CA"              'pointers to the two functions.  In the output
 Call ifX.Fx1() : Call ifX.Fx2()   'below pVTbl=1280208 represents a memory address
 Call ifY.Fy1() : Call ifY.Fy2()   'where the number 4200192 is stored - and 4200192
                                   'is the base address of the I_X Virtual Function
 'Call methods using               'Table.  If one sets another Dword Ptr variable
 'interface/vtable                 'such as VTbl to this base address in the VTable,
 'pointers.  1st I_X               'one will be able to step through the VTable in
 pVTbl=ObjPtr(ifX)                 'four byte increments using base pointer
 Print "pVTbl      = " pVTbl       'subscript notation, i.e., @VTbl[i], and output
 VTbl=@pVTbl[0]                    'either the pointer address in the VTable -
 Print "VTbl       = " VTbl        'Varptr(@VTbl[i]), or the function/method address
 Print:Print                       'being pointed to - @VTbl[i].
 Print " i         Varptr(@VTbl[i])  @VTbl[i]     Call Dword @VTbl[i]"
 Print "========================================================================="
 For i=0 To 4
   If i<=2 Then
      Print i, Varptr(@VTbl[i]), @VTbl[i]; "     IUnknown Fns (better not call!)"
   Else                                          'The reason we had better not try to
      Print i, Varptr(@VTbl[i]), @VTbl[i],;      'call the IUnknown procedures in the same
      Call DWord @VTbl[i] Using pFn(0)           'manner as we are calling the I_X and I_Y
   End If                                        'methods that we have implemented is that
 Next i                                          'we are using function pointers to call
 Print:Print                                     'these functions, and to do that one needs
                                                 'a properly configured function pointer
 'Then I_Y                                       'definition. At top our pFn Declare won't
 pVTbl=ObjPtr(ifY)                               'work for the IUnknown functions as their
 Print "pVTbl      = " pVTbl                     'function signatures are quite different.
 VTbl=@pVTbl[0]                                  'If you try to call them a crash will be
 Print "VTbl       = " VTbl                      'quite imminent in your future.
 Print
 Print " i         Varptr(@VTbl[i])  @VTbl[i]     Call Dword @VTbl[i]"
 Print "========================================================================="
 For i=0 To 4
   If i<=2 Then
      Print i, Varptr(@VTbl[i]), @VTbl[i]; "     IUnknown Fns (better not call!)"
   Else                                          'One other issue observant readers may note
      Print i, Varptr(@VTbl[i]), @VTbl[i],;      'is that the pFn Declare shows a DWord
      Call DWord @VTbl[i] Using pFn(0)           'parameter in the declaration of the function
   End If                                        'to be used in calling the interface functions,
 Next i                                          'but the interface functions lack any function
 Waitkey$                                        'parameters.  This is due to the fact that all
                                                 'OOP implementations pass a hidden class pointer
 PBMain=0                                        'as the first argument of all implemented
End Function                                      'function calls.  If I didn't use this dummy
                                                 'argument the program would likely crash after
                                                 'the function call when a non-existent parameter
'Called Fx1()                                     'would be popped off the stack.
'Called Fx2()
'Called Fy1()
'Called Fy2()
'
'pVTbl      =  1280208
'VTbl       =  4200192
'
' i         Varptr(@VTbl[i])  @VTbl[i]     Call Dword @VTbl[i]
'=========================================================================
' 0             4200192       4208481      IUnknown Fns (better not call!)
' 1             4200196       4208461      IUnknown Fns (better not call!)
' 2             4200200       4208604      IUnknown Fns (better not call!)
' 3             4200204       4198799      Called Fx1()
' 4             4200208       4198867      Called Fx2()
'
'pVTbl      =  1280340
'VTbl       =  4200152
'
' i         Varptr(@VTbl[i])  @VTbl[i]     Call Dword @VTbl[i]
'=========================================================================
' 0             4200152       4208481      IUnknown Fns (better not call!)
' 1             4200156       4208461      IUnknown Fns (better not call!)
' 2             4200160       4208604      IUnknown Fns (better not call!)
' 3             4200164       4198935      Called Fy1()
' 4             4200168       4199003      Called Fy2()      


    I believe there may be some useful material in the above simple program, and I believe fully understanding it may be a prerequisite to understanding what follows.  What follows is an attempt to create an actual component in C++ containing the I_X and I_Y interfaces which we'll take apart piece by piece in our   client PowerBASIC and C++ programs. And in our client programs we'll be able to call the IUnknown functions harmlessly and generate output statements from within them – unlike in the above program.  The only slight modification to the I_X and I_Y interfaces from that above is that I added an integer parameter to the Fx() / Fy() functions – which parameter value just gets printed out to the console along with the message that the function was called.  Below brace yourself for not one, not two, not even three, but for no less than seven C++ source files that comprise the code behind this simple COM object (some are real short).  The seven files are as follows with brief descriptions of what is in each one.  I will attach all this stuff in a pkzip file for you....


Server.cpp    This file comprises the component's dll housing and contains the only exported functions (4) and DllMain().
CA.cpp        This file consists of the implementations of component CA, its Class Factory, and the two interfaces.
Registry.cpp  This file contains boilerplate registry code for registering and unregistering the component
CA.h          Declarations of class CA and class CAClassFactory
Ifunctions.h  Guids and declarations of abstract base classes I_X and I_Y that are implemented in CA.cpp
Registry.h    Declarations of RegisterServer() and UnregisterServer() so they can be referenced in Server.cpp
CA.def        Module Definition File listing the four exported functions so the linker can export them.


Now for the files...

Server.cpp

//Server.cpp            //Note that all the functions in this file are exported.  That's what
#include <windows.h>    //STDAPI in C mumbo jumbo stands for.  Its a macro that expands to
#include <initguid.h>   //extern "C" HRESULT __export __stdcall. The extern "C" part tells the
#include "CA.h"         //C++ compiler not to mangle the function names so that other system
#include "Registry.h"   //code can recognize these names in a GetProcAddress() call.

//Globals
HINSTANCE  g_hModule           = NULL;              //Store dll instance handle
const char g_szFriendlyName[]  = "Com Object CA";   //Store friendly name of component
const char g_szVerIndProgID[]  = "ComObject.CA";    //Store Version Independent ProgID
const char g_szProgID[]        = "ComObject.CA.1";  //Store Versioned Program ID.
long       g_lObjs             = 0;                 //count of outstanding objects
long       g_lLocks            = 0;                 //used to keep server from unloading


STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, void** ppv)  
{                                    
CAClassFactory* pCF=NULL;           //DllGetClassObject() is critically important because
HRESULT hr;                         //it is this function that the operating system calls
                                    //when a client attempts to instantiate an object
if(rclsid!=CLSID_CA)                //contained within this dll through a CoCreateInstance()
   return E_FAIL;                   //call, or, in the case of the PowerBASIC compiler when
pCF=new CAClassFactory;             //it internally makes the call on the client's behalf.
if(pCF==0)                          //Whether PowerBASIC does it or we do it through a
   return E_OUTOFMEMORY;            //CoCreateInstance() call, the operating system locates
hr=pCF->QueryInterface(riid,ppv);   //the path to the dll containing some specific CLSID
if(FAILED(hr))                      //from the \CLSID\InProcServer32 key in the registry,
   delete pCF;                      //does a LoadLibrary() and GetProcAddress() call on the
                                    //dll, and then calls DllGetClassObject().  If the CLSID
return hr;                          //passed in matches CLSID_CA, then the C++ 'new' operator
}                                    //is used to create an instance of CA's Class Factory (in...


STDAPI DllCanUnloadNow()             //CA.cpp.  Having done that it then checks to see if it
{                                    //can obtain an IUnknown or IClassFactory pointer on CA's
if(g_lObjs||g_lLocks)               //Class Factory.  If it can the reference count on the
   return S_FALSE;                  //class factory is incremented.  What happens next is
else                                //critical.  COM internal system code now calls
   return S_OK;                     //CAClassFactory::CreateInstance() and that is what
}                                    //actually causes a new instance of the CA class to be...


STDAPI DllRegisterServer()           //created - which creation causes memory allocations for
{                                    //both the class's VTable pointers and the VTables
return RegisterServer               //themselves.  Further, class CA implements the abstract
(                                   //base classes I_X and I_Y, and pointers to the  
 g_hModule,                         //implementations of the pure virtual functions contained
 CLSID_CA,                          //within these base classes are placed in the respective
 g_szFriendlyName,                  //VTable of each interface.  At this point we have an  
 g_szVerIndProgID,                  //object in memory which can be called to do work for us.
 g_szProgID
);                                  //DllRegisterServer() and DllUnregisterServer are for
}                                    //adding or eliminating class ids, program ids, etc.,...


STDAPI DllUnregisterServer()         //relating to the component from the registry.  They are
{                                    //called by the RegSvr32.exe Windows component.  For
return UnregisterServer             //example, on this development machine CA.dll is in
(                                   //C:\Code\VStudio\VC++6\Projects\COM\ComObjects\CA\Release\CA.dll
 CLSID_CA,                          //I'd register it by doing this...
 g_szVerIndProgID,
 g_szProgID                         //RegSvr32 C:\Code\VStudio\VC++6\Projects\COM\ComObjects\CA\Release\CA.dll
);
}                                    //Having done that I thankfully saw a message box telling...


BOOL APIENTRY DllMain(HINSTANCE hInst, DWORD reason, LPVOID reserved)
{
switch (reason)                     //me registration was successful.  Registry code is pretty
{                                   //horrible and in this code was relegated to Registry.cpp
 case DLL_PROCESS_ATTACH:           //Note that that code needs the dll's instance handle
   g_hModule=hInst;                 //and that is saved just at left in DllMain in a global
   break;                           //variable just for that purpose.
 case DLL_PROCESS_DETACH:
   break;
}

return TRUE;                        //Returning FALSE causes the dll to not load.
}
//End Server.cpp



CA.cpp

//CA.cpp              //If you don't mind doing command line compiling with  
#include <windows.h>  //Microsoft C++ 6 you can use this command line...
#include <stdio.h>    //
#include "CA.h"       //cl /LD CA.cpp Server.cpp Registry.cpp UUID.lib Advapi32.lib Ole32.lib CA.def


CA::CA()                         //CA Constructor.  The creation/instantiation of every
{                                //object will cause a thread safe increment of the global
m_lRef=0;                       //count of CA objects created in the Server.  The CA
InterlockedIncrement(&g_lObjs); //member variable m_lRef will count the number of outstanding
}                                //interface pointers handed out through QueryInterface().


CA::~CA()
{                                //When a CA object is destroyed the count of objects is  
InterlockedDecrement(&g_lObjs); //decremented by the thread safe InterlockedDecrement()
}                                //function.


HRESULT __stdcall CA::QueryInterface(REFIID riid, void** ppv)
{
*ppv=0;                  //Store initial null at address pointed to by ppv.
if(riid==IID_IUnknown)   //If GUID of either IID_IUnknown or IID_IX
   *ppv=(I_X*)this;      //is passed 'in', cast this pointer to point
else if(riid==IID_I_X)   //to the 1st of the two pVTbl pointers that constitute
   *ppv=(I_X*)this;      //the class.  If client wants an IID_IY interface, return
else if(riid==IID_I_Y)   //the 2nd pointer - which pointer points to the IY
   *ppv=(I_Y*)this;      //VTable.  If *ppv has been set, then do an AddRef() and
if(*ppv)                 //return S_OK.  Otherwise, return E_NOINTERFACE.  The
{                        //printf statement below will only execute if a non-
   AddRef();             //supported interface IID was passed in through the
   return S_OK;          //REFIID parameter.  This is how the Called CA::QueryInterface()
}                        //output was generated in the client app through a function
printf("Called CA::QueryInterface()\n");  //pointer call.

return(E_NOINTERFACE);
}


ULONG __stdcall CA::AddRef()            //The member variable of class CA this->m_lRef
{                                       //counts interface pointers handed out by
printf("Called CA::AddRef()\n");       //QueryInterface().  InterlockedIncrement takes
return InterlockedIncrement(&m_lRef);  //a pointer parameter so the address ( & ) of
}                                       //this->m_lRef is passed in.


ULONG __stdcall CA::Release()           //When you are done using an interface pointer
{                                       //Release() should be called on the pointer so
printf("Called CA::Release()\n");      //that the count of references to the object can
if(InterlockedDecrement(&m_lRef)==0)   //be decremented.  When the count reaches zero
{                                      //the class automatically deletes itself from
   delete this;                        //memory., i.e., delete this;.  However, the
   return 0;                           //dll server may still reside in memory if the
}                                      //global count of objects created is greater
                                       //than zero, i.e., g_lObjs isn't zero..  
return m_lRef;
}


HRESULT __stdcall CA::Fx1(int iNum)             //These functions just output a
{                                               //message that they were called
printf("Called Fx1()  :  iNum = %u\n",iNum);   //plus the value of the parameter
return S_OK;                                   //passed in.  Fx1() and Fx2() are
}                                               //part of the I_X interface/vtable


HRESULT __stdcall CA::Fx2(int iNum)             //and Fy1() and Fy2() part of the
{                                               //I_Y vtable, i.e., these respective
printf("Called Fx2()  :  iNum = %u\n",iNum);   //vtables contain pointers to these
return S_OK;                                   //functions.
}


HRESULT __stdcall CA::Fy1(int iNum)
{
printf("Called Fy1()  :  iNum = %u\n",iNum);
return S_OK;
}


HRESULT __stdcall CA::Fy2(int iNum)
{
printf("Called Fy2()  :  iNum = %u\n",iNum);
return S_OK;
}


CAClassFactory::CAClassFactory()  //CAClassFactory Constructor.  Four procedures are exported from
{                                 //a com server.  They are DllGetClassObject(), DllCanUnloadNow()
m_lRef=0;                        //DllRegisterServer(), and DllUnregisterServer().  When a
}                                 //client app calls CoCreateInstance() with the CLSID of a....
                                 

CAClassFactory::~CAClassFactory() //component that can be located in the registry, code internal
{                                 //to COM's implementation will do a LoadLibrary() and GetProcAddress()
//MathClassFactory Destructor    //on DllGetProcAddress().  See my discussion in Server.cpp about this.
}


HRESULT __stdcall CAClassFactory::QueryInterface(REFIID riid, void** ppv)
{
*ppv=0;
if(riid==IID_IUnknown || riid==IID_IClassFactory)  //In this component that doesn't do very much,
   *ppv=this;                                      //the whole class factory thing seems kind of
if(*ppv)                                           //superfluous.  In other words, when COM Services
{                                                  //calls DllGetClassObject(), why not just create
   AddRef();                                       //the component CA directly with the C++ new
   return S_OK;                                    //operator instead of 1st creating a class
}                                                  //factory which in this case does absolutely
                                                   //nothing but create and destroy itself (and  
return E_NOINTERFACE;                              //uses new to create CA in the process)?  Well,
}                                                   //I suppose you can think of the class factory...


ULONG __stdcall CAClassFactory::AddRef()            //concept something like the WM_CREATE message
{                                                   //in Windows Api coding.  In that context one
return InterlockedIncrement(&m_lRef);              //usually creates the various user interface
}                                                   //elements, i.e., buttons, labels, etc., that...


ULONG __stdcall CAClassFactory::Release()           //will appear on the window being created.  That
{                                                   //is how the class factory can be used.  It does
if(InterlockedDecrement(&m_lRef)==0)               //whatever it takes to uniquely create the object.
{
   delete this;
   return 0;
}

return m_lRef;
}


HRESULT __stdcall CAClassFactory::CreateInstance(LPUNKNOWN pUnkOuter, REFIID riid, void** ppvObj)
{
HRESULT hr;                           //You won't find anywhere within the code here within any
CA* pCA;                              //of these project files where CAClassFactory::CreateInstance()
                                      //is called to create a new instance of class CA.  This
*ppvObj=0;                            //function gets called in a somewhat roundabout manner by
pCA=new CA;                           //the client app's CoCreateInstance() or CoGetClassObject()
if(pCA==0)                            //call.  See my discussion in Server.cpp.  First the Windows
   return E_OUTOFMEMORY;              //COM subsystem loads the dll from info it obtains on the
hr=pCA->QueryInterface(riid,ppvObj);  //class from the registry.  Then it calls DllGetClassObject(),
if(FAILED(hr))                        //which is an exported function.  Internal Windows code then
   delete pCA;                        //creates a CAClassFactory instance and from that instance
                                      //calls the CAClassFactory::CreateInstance() code you see  
return hr;                            //here.  And just left you see where a 'new' CA is created,
}                                      //and a QueryInterface() done on it to see if riid is there.


HRESULT __stdcall CAClassFactory::LockServer(BOOL fLock)
{
if(fLock)                             //This function can be called to lock the dll in memory
   InterlockedIncrement(&g_lLocks);   //even if no outstanding instances of CA are present.  This
else                                  //works good in cases where you need to create and destroy
   InterlockedDecrement(&g_lLocks);   //objects without having to reload the dll between creation/
                                      //destruction cycles.
return S_OK;
}
//End CA.cpp


Ifunctions.h

//IFunctions.h
DEFINE_GUID(CLSID_CA,0x20000000,0x0000,0x0000,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04);
DEFINE_GUID(IID_I_X,0x20000000,0x0000,0x0000,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05);
DEFINE_GUID(IID_I_Y,0x20000000,0x0000,0x0000,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06);

interface I_X : IUnknown                  //In C++ lingo an interface declaration such as this is known
{                                         //as an abstract base class.  It can't be instantiated directly
virtual HRESULT __stdcall Fx1(int)=0;    //because it only contains what are known as pure virtual
virtual HRESULT __stdcall Fx2(int)=0;    //functions (note the '=0' part).  What is actually going on  
};                                        //here is the creation of a specific footprint in memory that...


interface I_Y : IUnknown                  //COM uses as its fundamental basis of operation.  Let me
{                                         //elaborate.  The C++ language was created long before COM and
virtual HRESULT __stdcall Fy1(int)=0;    //most implementations of the language allocate a pointer
virtual HRESULT __stdcall Fy2(int)=0;    //within a class for each abstract base class from which it
};                                        //inherits.  If you look just below and left you'll see that class
//End Ifunctions.h                        //CA inherits publiclly from interfaces I_X and I_Y.  In C++...
                       



//CA.h                                    //the term interface means the same thing as a struct, which
#include "IFunctions.h"                   //in PowerBASIC means the same thing as a good old BASIC TYPE.  
extern long g_lObjs;                      //This #define macro can be found in objbase.h.  Further, in  
extern long g_lLocks;                     //C++ structs are almost the same thing as classes except that

class CA : public I_X, public I_Y         //all their members are public by default, i.e., visible in
{                                         //inheriting classes.  That is the simple reason they are used.
public:                                  //Now, when class CA is constructed by most C++ compilers a memory
CA();            //Constructor           //allocation will be made for two pointers that point to other
virtual ~CA();   //Destructor            //blocks of memory where pointers to the implemented functions

//Iunknown Functions                     //will be stored.  This is the virtual function table.  Our
virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv);   //client programs will show its
virtual ULONG   __stdcall AddRef();      //exact structure in withering detail.  Suffice it to say here  
virtual ULONG   __stdcall Release();     //though that the block of memory CA allocates for interface I_X
                   
//I_X Interface Methods/Functions        //(its virtual function table) will be 20 bytes, and the same  
virtual HRESULT __stdcall Fx1(int);      //for interface I_Y.  This is because these interfaces, each of
virtual HRESULT __stdcall Fx2(int);      //which only contains two functions, themselves inherit from  

//I_ Interface Methods/Functions         //another struct, i.e., IUnknown, which itself contains pointers
virtual HRESULT __stdcall Fy1(int);      //to three functions – QueryInterface(), AddRef(), and Release().
virtual HRESULT __stdcall Fy2(int);      //This is the absolute most fundamental rule in the COM standard

protected:                               //that all interfaces, i.e., structs, classes, virtual function
long m_lRef;                             //tables, whatever you want to call them – must have pointers to
};                                        //QueryInterface(), AddRef(), and Release() as their first three...


class CAClassFactory : public IClassFactory                           //members.  You can easily see that  
{                                                                     //to be true by just looking directly
public:                                                              //left at the two classes there that
CAClassFactory();                                                    //contain these functions.  Note that
virtual ~CAClassFactory();                                           //it may not look like these functions

public:                                                              //are at position one because they are
//IUnknown                                                           //not first in the declarations, but that
virtual HRESULT __stdcall QueryInterface(REFIID, void**);            //is somewhat confusing because the order
virtual ULONG   __stdcall AddRef();                                  //of declaration there isn't the
virtual ULONG   __stdcall Release();                                 //controlling factor, but rather the

//IclassFactory                                                      //controlling factor is the inheritance
virtual HRESULT __stdcall CreateInstance(LPUNKNOWN, REFIID, void**); //chain which forms the basis of the
virtual HRESULT __stdcall LockServer(BOOL);                          //class.  At left CAClassFactory inherits

protected:                                                           //from IclassFactory which is a system
//Reference Count                                                    //defined class.  And in the declaration
long  m_lRef;                                                        //of IclassFactory it would be found that
};                                                                    //it itself inherits from IUnknown.
//End CA.h


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.cpp

// Registry.cpp
#include <objbase.h>
const int 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 HKEY_CLASSES_ROOT.
   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 ;
}
//End Registry.cpp




CA.def -- I haven't been able to figure out how to successfully get functions exported from COM objects without these darn    
//        .def files.  I think it might be possible, but nothing I've tried works, including but not limited to various
//        combinations of __declspec(dllexport), extern "C", etc., etc., etc.

;//CA.def
LIBRARY      "CA"
DESCRIPTION  "CA Windows Dynamic Link Library"
EXPORTS
    DllGetClassObject     PRIVATE
    DllCanUnloadNow       PRIVATE
    DllRegisterServer     PRIVATE
    DllUnregisterServer   PRIVATE

Frederick J. Harris

#1
....continued

     Well, pretty intimidating I guess.  I'll try to explain it – particularly how the class is instantiated through COM services and DllGetClassObject().  Note that the first file I listed was Server.cpp and DllGetClassObject() is the first function in that file. 

     If you have Visual C++ 6 and your environment is set up for command line compiling you can easily compile it into a dll with the following command line...

cl /LD CA.cpp Server.cpp Registry.cpp UUID.lib Advapi32.lib Ole32.lib CA.def

     Alternately, you can create a blank dll project in the IDE, add the files, and compile & link through the IDE.  There is even an option within the IDE to register the dll.  Or, you can just use regsvr32 on the attached CA.dll I'll attach to this post.

     To create the dll with the huge, slow, lumbering, ponderous bulk of VC 9 (Visual C++ 2008), you'll need to fight your way through a nearly impenetrable tangle of project property sheets until you get to a place where you can set the default character set to be used to be something other than UNICODE (multy-byte or NOT SET), and you'll have to let the linker know about the module definition file containing the exports.  Here are the steps:


1) Create Dll Project In VStudio C++ 2008;

2) Add all CA files discussed here to project;

3) Open Properties Sheet for project CA and under
   CA Property Pages \Configuration Properties \General \Project Defaults \Character Set...
   Choose Not Set or Multi-Byte Character Set;

4) Under \Configuration Properties \Linker \Input \Module Definition File...
   Set the Module Definition File to CA.def;

5) Use my RegistryAlt.cpp file instead of Registry.cpp.  Rename it to Registry.cpp.


     Finally, although it will compile & link without doing this, you'll get piles of security warnings due to the use of strcpy(), strcat(), and wcstombs() in Registry.cpp.  I'll attach an alternate Registry.cpp (RegistryAlt.cpp) in the attachments that will compile clean. 

     If you want to attempt to create the dll with the GNU compiler collection, contact me directly & I'll try to help.  There are some issues. 

     Following is a whirlwind tour of how the class CA gets created so that a client can call the interface functions.  First, the dll needs to be registered with regsvr32 (or you can do it manually or in code yourself but I doubt you would want to do that).  What the registration process does is store the program ids (ComObject.CA.1) under HKEY_CLASSES_ROOT and the path to the dll file in HKEY_CLASSES_ROOT\CLSID\{20000000-0000-0000-0000-000000000004}\InProcServer32.  The registered path to CA.dll on the machine on which I'm writing this is...

C:\Code\Vstudio\VC++6\Projects\COM\ComObjects\CA\Release\CA.dll

     In our PowerBASIC program I'll soon make the following function call...

Local pVTbl As Dword Ptr
Call CoCreateInstance($CLSID_CA, Byval %NULL, %CLSCTX_INPROC_SERVER, $IID_IUnknown, pVTbl)

     The chain of events that will ensue is as follows.  The operating system will search the registry for a registered in process component ( a dll )with a class id of $CLSID_CA.  This is the 16 byte number listed just above.  When it locates this object in the registry it will have the path to the dll listed also above.  With that information it can do a LoadLibrary() on the Dll and a GetProcAddress() on the exported function DllGetClassObject() from Server.cpp.  At this point you need to take a close look at DllGetClassObject() in my provided code (and also remember where we are at right now, and that is inside a system call, i.e., CoCreateInstance().  Inside DllGetClassObject() a CAClassFactory pointer (CAClassFactory* pCF) is allocated.  This can be done because we are inside DllGetClassObject and DllGetClassObject is inside the Dll where the CAClassFactory class is both defined and implemented.  Having obtained this pointer the C++ 'new' operator is used to create a new instance in memory of the CAClassFactory class.  The 'new' operator in C++ supplanted malloc from C, and is its new memory allocation mechanism.  Having created the class, QueryInterface() on the class factory is called and the riid parameter will be the IID of the class factory, and a pointer returned as the last parameter of a successful call will be a pointer to the class factory, i.e., ppv.         
       
hr=pCF->QueryInterface(riid,ppv);

     One of the two interface members (beyond the three of IUnknown) of IClassFactory is CreateInstance() and within CAClassFactory::CreateInstance() is another use of the C++ new operator but this time to create a new instance of class CA – our container class of our desired I_X and I_Y interfaces.  This part is confusing and after I have presented several programs and discussed COM memory & low level access techniques in some detail I will return to the subject to try to improve your understanding of it.  For now, lets just take a look at several programs which access the class CA and its interfaces.  First the PowerBASIC Console Compiler 5.0 program, then the same exact thing in C++...


#Compile               Exe "CAClient.exe"
#Dim                   All
#Include               "Win32Api.inc"
%CLSCTX_INPROC_SERVER  =&H1???
$IID_IUnknown          =Guid$("{00000000-0000-0000-C000-000000000046}")  'Microsoft Defined
$CLSID_CA              =Guid$("{20000000-0000-0000-0000-000000000004}")  'Class ID of Class CA, ie., Class A
$IID_IX                =Guid$("{20000000-0000-0000-0000-000000000005}")  'Interface X
$IID_IY                =Guid$("{20000000-0000-0000-0000-000000000006}")  '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 Any) 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,pUnk As Dword Ptr
  Local hResult As Long
  Register i As Long

  hResult=CoCreateInstance($CLSID_CA, Byval %NULL, %CLSCTX_INPROC_SERVER, $IID_IUnknown, pVTbl)
  If SUCCEEDED(hResult) Then
     Print "CoCreateInstance() For IUnknown On " ProgID$($CLSID_CA) " 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(pVTbl) To hResult
  End If
  Waitkey$

  PBMain=0
End Function



'Called CA::AddRef()
'Called CA::AddRef()
'Called CA::Release()
'Called CA::AddRef()
'Called CA::Release()
'CoCreateInstance() For IUnknown On ComObject.CA.1 Succeeded!
'pVTbl =  9569824
'
'Varptr(@pVTbl[i])  Varptr(@VTbl[i])  @VTbl[i]     Function Call With Call Dword
'===============================================================================
'9569824            268464412         268439632    Called CA::QueryInterface()
'9569824            268464416         268439776    Called CA::AddRef()
'9569824            268464420         268439808    Called CA::Release()
'9569824            268464424         268439888    Called Fx1()  :  iNum = 0
'9569824            268464428         268439920    Called Fx2()  :  iNum = 0
'
'9569828            268464392         268440384    Called CA::QueryInterface()
'9569828            268464396         268440400    Called CA::AddRef()
'9569828            268464400         268440416    Called CA::Release()
'9569828            268464404         268439952    Called Fy1()  :  iNum = 1
'9569828            268464408         268439984    Called Fy2()  :  iNum = 1
'
'Called CA::Release()
 


     This particular version of the program must use PowerBASIC's includes because Jose's includes declare CoCreateInstance() somewhat differently.  Here is a version that will work with Jose's includes...



#Compile               Exe "CAClient.exe"
#Dim                   All
#Include               "Windows.inc"
#Include               "ObjBase.inc"
%CLSCTX_INPROC_SERVER  =&H1???
$IID_IUnknown          =Guid$("{00000000-0000-0000-C000-000000000046}")  'Microsoft Defined
$CLSID_CA              =Guid$("{20000000-0000-0000-0000-000000000004}")  'Class ID of Class CA, ie., Class A
$IID_IX                =Guid$("{20000000-0000-0000-0000-000000000005}")  'Interface X
$IID_IY                =Guid$("{20000000-0000-0000-0000-000000000006}")  '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_CA, Nothing, %CLSCTX_INPROC_SERVER, $IID_IUnknown, pVTbl)
  If SUCCEEDED(hResult) Then
     Print "CoCreateInstance() For IUnknown On " ProgID$($CLSID_CA) " 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(pVTbl) To hResult
  End If
  Waitkey$

  PBMain=0
End Function

'Called CA::AddRef()
'Called CA::AddRef()
'Called CA::Release()
'Called CA::AddRef()
'Called CA::Release()
'CoCreateInstance() For IUnknown On ComObject.CA.1 Succeeded!
'pVTbl =  9568672
'
'Varptr(@pVTbl[i])  Varptr(@VTbl[i])  @VTbl[i]     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
'
'Called CA::Release()



     And finally, here is an exact translation of the above into C++...


#include <objbase.h>  //CAClient
#include <stdio.h>
const CLSID CLSID_CA ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04}}; //Class ID of Class CA, ie., Class A
const IID   IID_I_X  ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05}}; //Interface X
const IID   IID_I_Y  ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06}}; //Interface Y
const IID   IID_Junk ={0x12345678,0x9876,0x5432,{0x12,0x34,0x56,0x78,0x98,0x76,0x54,0x32}}; //Junk Number So QueryInterface() Fails


HRESULT (__stdcall* ptrQueryInterface) (int, const IID&, void**);  //these are C function pointers somewhat analogous to
ULONG   (__stdcall* ptrAddRef)         (int);                      //PowerBASIC's Call Dword setup (but not as easy to
ULONG   (__stdcall* ptrRelease)        (int);                      //understand!!!!!).
void    (__stdcall* pIFn)              (int,int);


interface I_X : IUnknown
{
virtual HRESULT __stdcall Fx1(int)=0;
virtual HRESULT __stdcall Fx2(int)=0;
};


interface I_Y : IUnknown
{
virtual HRESULT __stdcall Fy1(int)=0;
virtual HRESULT __stdcall Fy2(int)=0;
};


int main(void)
{
IUnknown*       pIUnk=NULL;
unsigned int*   pVTbl=0;
unsigned int*   VTbl=0;
unsigned int    i=0;
HRESULT         hr;

hr=CoInitialize(NULL);
if(SUCCEEDED(hr))
{
    hr=CoCreateInstance(CLSID_CA,NULL,CLSCTX_INPROC_SERVER,IID_IUnknown,(void**)&pVTbl);
    if(SUCCEEDED(hr))
    { 
       puts("CoCreateInstance() For IUnknown Succeeded!");
       printf("pVTbl   = %u\n",pVTbl);
       printf("\n");
       printf("&pVTbl[i]\t&VTbl[i]\tVTbl[i]\t\tFunction Call Through Pointer\n");
       printf("=============================================================================\n");
       for(i=0;i<=1;i++)
       {
           VTbl=(unsigned int*)pVTbl[i];                                             //Call...
           printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[0],VTbl[0]);   
           ptrQueryInterface=(HRESULT(__stdcall*)(int, const IID&, void**)) VTbl[0];
           ptrQueryInterface((int)&pVTbl[i],IID_Junk,(void**)&pIUnk);                //QueryInterface()
           printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[1],VTbl[1]);                       
           ptrAddRef=(ULONG(__stdcall*)(int)) VTbl[1];
           ptrAddRef((int)&pVTbl[i]);                                                //AddRef()
           printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[2],VTbl[2]);                       
           ptrRelease=(ULONG(__stdcall*)(int)) VTbl[2];
           ptrRelease((int)&pVTbl[i]);                                               //Release()
           printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[3],VTbl[3]);                       
           pIFn=(void(__stdcall*)(int,int)) VTbl[3];
           pIFn((int)&pVTbl[i],i);                                                   //Fx1() / Fy1()   
           printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[4],VTbl[4]);                       
           pIFn=(void(__stdcall*)(int,int)) VTbl[4];
           pIFn((int)&pVTbl[i],i);                                                   //Fx2() / Fy2()   
           printf("\n");
       }
       ptrRelease=(ULONG(__stdcall*)(int)) VTbl[2];
       ptrRelease((int)&pVTbl[1]);
    }
    CoUninitialize();
    getchar();
}

return 0;
}


/*
Called CA::AddRef()
Called CA::AddRef()
Called CA::Release()
Called CA::AddRef()
Called CA::Release()
CoCreateInstance() For IUnknown Succeeded!
pVTbl   = 10355664

&pVTbl[i]       &VTbl[i]        VTbl[i]         Function Call Through Pointer
=============================================================================
10355664        268464396       268439696       Called CA::QueryInterface()
10355664        268464400       268439840       Called CA::AddRef()
10355664        268464404       268439872       Called CA::Release()
10355664        268464408       268439936       Called Fx1()  :  iNum = 0
10355664        268464412       268439968       Called Fx2()  :  iNum = 0

10355668        268464376       268440448       Called CA::QueryInterface()
10355668        268464380       268440464       Called CA::AddRef()
10355668        268464384       268440480       Called CA::Release()
10355668        268464388       268440000       Called Fy1()  :  iNum = 1
10355668        268464392       268440032       Called Fy2()  :  iNum = 1

Called CA::Release()
*/


     Lets start near the top.  Right after the GUIDs and includes are these declare statements...

Declare Function ptrQueryInterface(Byval this As Dword, Byref iid As Guid, Byref pUnknown As Any) 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

     We are going to be using low level procedural code to access objects created with an OOP language, that is, C++, and in order to do that it will be necessary to call class methods through function pointers.  Jose has been doing this sort of thing for years in his work with COM in PowerBASIC, and because of his complete mastery of these techniques and his generosity in providing his code to us, we have been able to access ActiveX controls relatively easily with PowerBASIC.  But make no mistake; understanding this function pointer access is fundamental.  To understand why we need these declares and function pointers for the type of low level access we are doing lets examine the concept of inheritance from a slightly different angle from that in which it is usually presented, and that angle is the effect of inheritance on the memory layout of inheriting classes.

     Say we have a BASIC TYPE like so...


     Type SomeType
       A As Integer
       B As Integer
       C As Integer
     End Type     


     And then another type...


     Type Another
       BasicInterface As SomeType
       D As Integer
       E As Integer
     End Type


     What this will look like in memory in terms of Type Another is pretty easy to imagine as we have five 16 bit integers that in total amounts to ten bytes. The first six bytes are A, B, and C of BasicInterface As SomeType, and the last four bytes are D and E of Another.  This is a simple example of inheritance and can be found frequently in C language usage.  One of the most popular GUI programming toolkits in the Linux world is GTK, and that toolkit is a C language based toolkit where use of this type of inheritance and terminology is common.  Now take a look at how our simple class CA might be defined in PowerBASIC, and then in C++ from CA.h...

1st PowerBASIC
     
Class CA
  Interface I_X : Inherit IUnknown   '...function can be used to obtain the
    Method Fx1()                     'address of each interface's VTbl.  Where
      Print "Called Fx1()"           'PowerBASIC seems to differ somewhat from
    End Method                       'C++ is that in this situation in C++ the
                                     'Sizeof(CA) would be 8 and those 8 bytes
    Method Fx2()                     'would be two contiguous VTable pointers.
      Print "Called Fx2()"           'PowerBASIC will return two interface
    End Method                       'pointers also but they do not appear to
  End Interface                      'be contiguous.  Below the I_X pVTbl is
                                     '1280208 and the I_Y interface pointer is
  Interface I_Y : Inherit IUnknown   'at 1280340 - not 1280212.  Nontheless
    Method Fy1()                     'they each can be used to obtain the
      Print "Called Fy1()"           'address of each interface's VTable, and
    End Method                       'the function pointers held in the VTables
                                     'can be used to call the interface
    Method Fy2()                     'functions.  This probably isn't really
      Print "Called Fy2()"           'recommended but this exercise at least
    End Method                       'shows the memory layout.
  End Interface
End Class


Then C++ from CA.h


interface I_X : IUnknown
{
virtual HRESULT __stdcall Fx1(int)=0;
virtual HRESULT __stdcall Fx2(int)=0;
};


interface I_Y : IUnknown
{
virtual HRESULT __stdcall Fy1(int)=0;
virtual HRESULT __stdcall Fy2(int)=0;
};


     One of the things you will find in common between the PowerBASIC and C++ declarations is that the interfaces inherit from something named IUnknown, i.e.,


PowerBASIC:   Interface I_X : Inherit IUnknown   
C++:          interface I_X : IUnknown


     What is happening here is that these memory structures are having a pre-existing memory structure prepended to themselves just as our TYPE Another had the three A, B, and C integers of SomeType prepended to its structure containing D and E.  Actually, IUnknown is either a C struct or a C++ class depending on whether the program is being compiled as a C or C++ program, and for our purposes here in PowerBASIC doing low level COM access is probably best translated as a Type something like this...


Type IUnknown
  QueryInterface As Dword Ptr
  AddRef         As Dword Ptr
  Release        As Dword Ptr
End Type


....and, since this type is being prepended to both interface I_X and I_Y, then the actual structure of each interface as a virtual function table laid out in memory from some base memory address is as follows for I_X...

base address + 0  pointer to QueryInterface()
base address + 4  pointer to AddRef()
base address + 8  pointer to Release()
base address + 12 pointer to Fx1()
base address + 16 pointer to Fx2()

If one were to have a pointer to this base address such as...

Local pVTbl As Dword Ptr
Local VTbl As Dword Ptr
Local i As Long

And you had code to beg, borrow or steal this pVTbl pointer from somewhere, you could iterate through this Vtable structure in memory like so and call the various functions...


VTbl=@pVTbl  'Get the base address of the virtual function table stored in a Vtable pointer
For i=0 To 4
  Call Dword @VTbl[i] Using SomeModelFunctionDefinition To hResult
Next I


     The only thing left undefined above is the model function definition to be used with the call Dword statement.  A different one is needed for each of the functions to be called, except that Fx1()/Fx2() and Fy1()/Fy2() can use the same one due to their having the same function signature.  I just told you all that so that you would understand what the declares are for at the top of the program.   

     Having flushed some of those details out let's take a look at the stat of the program where we call CoCreateInstance() to get our initial pVTbl on the COM object ComObject.CA.1 whose CLSID is $CLSID_CA:

hResult=CoCreateInstance($CLSID_CA, Byval %NULL, %CLSCTX_INPROC_SERVER, $IID_IUnknown, pVTbl)  'PowerBASIC's Includes

or

hResult=CoCreateInstance($CLSID_CA, pUnk, %CLSCTX_INPROC_SERVER, $IID_IUnknown, pVTbl)         'Jose's Includes

     The last output parameter will contain our initial pointer to the virtual function tables of this COM class.  But there is that whole issue again!  What is a COM class?  Well, in anything having to do with computers we're talking computer memory.  And when you are talking computer memory the most important questions are "Where is it and how big is it?"  That is actually easy to find out.  Let us alter the class code for class CA so as to print out the address of the object when it gets instantiated, and its size.  Up near the top of CA.cpp is the constructor for class CA as follows...


CA::CA()                         
{                               
m_lRef=0;                       
InterlockedIncrement(&g_lObjs);
}


We'll alter that as follows, recompile, then run the very same PowerBASIC program above...


CA::CA()                         
{                               
m_lRef=0;                       
InterlockedIncrement(&g_lObjs);
printf("sizeof(CA) = %u\n",sizeof(CA));
printf("this       = %u\n",this);
}


     The above CA class constructor will execute as soon as its class factory creates it in response to a client's CoCreateInstance() call, and the 1st printf call will output the size of the memory allocation for class CA, and the 2nd printf call will use the hidden 'this' pointer to output the base memory location of the allocation.  Re-running CAClient now produces this result...

sizeof(CA) = 12
this       = 9569824
Called CA::AddRef()
Called CA::AddRef()
Called CA::Release()
Called CA::AddRef()
Called CA::Release()
CoCreateInstance() For IUnknown On ComObject.CA.1 Succeeded!
pVTbl =  9569824

Varptr(@pVTbl)  Varptr(@VTbl)  @VTbl     Function Call With Call Dword
===============================================================================
9569824            268464396         268439696    Called CA::QueryInterface()
9569824            268464400         268439840    Called CA::AddRef()
9569824            268464404         268439872    Called CA::Release()
9569824            268464408         268439936    Called Fx1()  :  iNum = 0
9569824            268464412         268439968    Called Fx2()  :  iNum = 0

9569828            268464376         268440448    Called CA::QueryInterface()
9569828            268464380         268440464    Called CA::AddRef()
9569828            268464384         268440480    Called CA::Release()
9569828            268464388         268440000    Called Fy1()  :  iNum = 1
9569828            268464392         268440032    Called Fy2()  :  iNum = 1

Called CA::Release()
     
     In studying the output above there certainly is one particular number that kind of 'jumps out' at one due to its repetition in a number of seemingly important places.  That number is of course 9569824.  It was output both from within the Com object itself and as the value of pVTbl returned from the COM Server in the last parameter of CAClient's CoCreateInstance() call.  The other interesting number is '12'.   

     Lets start with the sizeof(CA)=12.  When a C++ class is instantiated which inherits from what are termed 'abstract base classes', the C++ compiler creates/allocates something termed a virtual function table pointer for each abstract base class from which it inherits.  If you look at the declaration of class CA in CA.h you'll see this...

class CA : public I_X, public I_Y
{
public:
CA();  //Constructor
virtual ~CA(); //Destructor

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

//I_X Interface Methods/Functions
virtual HRESULT __stdcall Fx1(int);
virtual HRESULT __stdcall Fx2(int);

//I_ Interface Methods/Functions
virtual HRESULT __stdcall Fy1(int);
virtual HRESULT __stdcall Fy2(int);

protected:
long m_lRef;
};

     The line...

     class CA : public I_X, public I_Y

...means that class CA is inheriting from two base classes which as it turns out are pure abstract base classes because there is no executable code within these classes.  Therefore class CA will have 8 bytes allocated for two pointers;  The 1st pointer will point to the I_X virtual function able and the second will point to the I_Y virtual function table.  The number 9569824 above is the address where the function table pointer to the I_X Vtable is stored, and that 32 but pointer will occupy bytes 9569824 through bytes 9569827.  The I_Y virtual function table pointer will be located in bytes 9569828 through bytes 9569831.  That accounts for 8 of the 12 bytes.  The last four bytes is for the single long data member contained within the class CA – long m_lRef.  The counter data member is there to count outstanding references made on the object.  In other words, every time you ask QueryInterface for a pointer to an existing object the reference counter will be incremented, and when a pointer is no longer used the reference counter will be decremented.  When it reaches zero the object automatically destroys itself.  I've got to say it is reassuring that the same number was returned from the object through the this pointer as we obtained in pVTbl through the return parameter!

     Now, stored at those 8 bytes in those two pointers beginning at 9569824 are two very important numbers.  If we were to do this...

Print @pVTbl[0]
Print @pVTbl[1]

     Our output would be this using the above example's numbers...

268464396
268464376

     The top number is where the I_X Vtable starts in memory (268464396), and the bottom number where the I_Y Vtable starts (268464376).  Recall we determined above that each of these interfaces/vtables will contain five 32 bit function pointers (three from IUnknown and two for the interface functions).  These function pointers will be arranges serially at four byte increments from the base pointer location.  You can see this in the 2nd column output under the Varptr(@VTbl) heading.  It can be deduced from this that when class CA was instantiated it made memory allocations for not only the 12 bytes previously discussed, but for each 20 byte Vtable.  However, it didn't count the Vtable memory in its allocation.  That memory would be counted within the interface class.  Also, the compiler had to allocate memory for the implemented functions within class CA, but memory allocated for functions isn't counted as part of a class's memory.  Pointers to the implemented functions however can be found located within each Vtable, and you can see these numbers in column three of the output under the @VTbl heading.  Note that these numbers have irregular spacing due to the fact that depending on the size of the compiled code, their location in memory is somewhat scattered.  However, the numbers must be good because when we used function pointers to call the functions through these addresses we obtained perfect results with perfect matching of AddRef and Release counts and no crashes.  By examining the code in CA.cpp for these functions you will be able to see how the output was generated.

continued......


Frederick J. Harris

#2
     Before we started with the client programs I mentioned I'd go into more detail at a later point concerning how class CA is instantiated within the dll as a result of the initial client CoCreateInstance() call.  If you are reasonably comfortable with what I have presented so far it may be time to delve into this matter in a bit more detail. 

     As I may have mentioned, CoCreateInstance() is a wrapper around some more basic COM functionality involving the IClassFactory interface.  Let's explore creating the class factory ourselves in the client app and calling CAClassFactory::CreateInstance() directly.  Its quite a bit easier in C++ than in PowerBASIC so lets start there so that you can get a feel for what it looks like, then we'll translate that to PowerBASIC.  Below is a version of the client series of programs I have been providing that has all error handling removed so as to concisely show the series of steps.  Its surprisingly easy.


#include <objbase.h> //CAClient5
#include <stdio.h>   //IID_IClassFactory={00000001-0000-0000-C000-000000000046};
const CLSID CLSID_CA ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04}}; //Class ID of Class CA, ie., Class A
const IID   IID_I_X  ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05}}; //Interface X
const IID   IID_I_Y  ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06}}; //Interface Y
const IID   IID_Junk ={0x12345678,0x9876,0x5432,{0x12,0x34,0x56,0x78,0x98,0x76,0x54,0x32}}; //Junk Number So QueryInterface() Fails


HRESULT (__stdcall* ptrQueryInterface) (int, const IID&, void**);
ULONG   (__stdcall* ptrAddRef)         (int);
ULONG   (__stdcall* ptrRelease)        (int);
void    (__stdcall* pIFn)              (int,int);


interface I_X : IUnknown
{
virtual HRESULT __stdcall Fx1(int)=0;
virtual HRESULT __stdcall Fx2(int)=0;
};


interface I_Y : IUnknown
{
virtual HRESULT __stdcall Fy1(int)=0;
virtual HRESULT __stdcall Fy2(int)=0;
};


int main(void)
{
IUnknown*       pIUnk=NULL;
IClassFactory*  pCF=NULL;
unsigned int*   pVTbl=0;
unsigned int*   VTbl=0;
 
CoInitialize(NULL);
CoGetClassObject(CLSID_CA,CLSCTX_INPROC_SERVER,NULL,IID_IClassFactory,(void**)&pCF);
pCF->CreateInstance(NULL,IID_IUnknown,(void**)&pVTbl);
pCF->Release();
printf("pVTbl   = %u\n",pVTbl);
printf("\n");
printf("&pVTbl[i]\t&VTbl[i]\tVTbl[i]\t\tFunction Call Through Pointer\n");
printf("=============================================================================\n");
for(unsigned int i=0;i<=1;i++)
{
     VTbl=(unsigned int*)pVTbl[i];                                             //Call...
     printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[0],VTbl[0]);   
     ptrQueryInterface=(HRESULT(__stdcall*)(int, const IID&, void**)) VTbl[0];
     ptrQueryInterface((int)&pVTbl[i],IID_Junk,(void**)&pIUnk);                //QueryInterface()
     printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[1],VTbl[1]);                       
     ptrAddRef=(ULONG(__stdcall*)(int)) VTbl[1];
     ptrAddRef((int)&pVTbl[i]);                                                //AddRef()
     printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[2],VTbl[2]);                       
     ptrRelease=(ULONG(__stdcall*)(int)) VTbl[2];
     ptrRelease((int)&pVTbl[i]);                                               //Release()
     printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[3],VTbl[3]);                       
     pIFn=(void(__stdcall*)(int,int)) VTbl[3];
     pIFn((int)&pVTbl[i],i);                                                   //Fx1() / Fy1()   
     printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[4],VTbl[4]);                       
     pIFn=(void(__stdcall*)(int,int)) VTbl[4];
     pIFn((int)&pVTbl[i],i);                                                   //Fx2() / Fy2()   
     printf("\n");
}
ptrRelease=(ULONG(__stdcall*)(int)) VTbl[2];
ptrRelease((int)&pVTbl[0]);
CoUninitialize();
getchar();

return 0;
}


/*
Called CA::AddRef()
pVTbl   = 10355664

&pVTbl[i]       &VTbl[i]        VTbl[i]         Function Call Through Pointer
=============================================================================
10355664        268464396       268439696       Called CA::QueryInterface()
10355664        268464400       268439840       Called CA::AddRef()
10355664        268464404       268439872       Called CA::Release()
10355664        268464408       268439936       Called Fx1()  :  iNum = 0
10355664        268464412       268439968       Called Fx2()  :  iNum = 0

10355668        268464376       268440448       Called CA::QueryInterface()
10355668        268464380       268440464       Called CA::AddRef()
10355668        268464384       268440480       Called CA::Release()
10355668        268464388       268440000       Called Fy1()  :  iNum = 1
10355668        268464392       268440032       Called Fy2()  :  iNum = 1

Called CA::Release()
*/


     The only significant change is that an IClassFactory* (IClassFactory Pointer) had to be declared, and instead of calling CoCreateInstance(), which returns a pointer to the IUnknown of class CA, we call CoGetClassObject() which returns an IUnknown pointer to CA's class factory, i.e., CAClassFactory.  Again, COM system code must load the dll from paths found in the registry to do this, and do a GetProcAddress() or whatever to perform a call to DllGetClassObject() on the client's behalf.  Then the client simply uses the returned pointer to the class factory – here pCF – to call CAClassFactory::CreateInstance().  By the way, the double colons in C++ are used here to specify that the function CreateInstance() is a member function of class CAClassFactory.  Its as simple as that in C++. 

IClassFactory*  pCF=NULL;

CoInitialize(NULL);
CoGetClassObject(CLSID_CA, CLSCTX_INPROC_SERVER, NULL, IID_IClassFactory,(void**)&pCF);
pCF->CreateInstance(NULL,IID_IUnknown,(void**)&pVTbl);
pCF->Release();

     One reason you might want to do this is if you are going to create multiple instances of the component you only need to create one class factory just once and use it to create as many components as you want.  Under those circumstances it would be wasteful to use CoCreateInstance() multiple times.

     It was a bit of a pain translating this to PowerBASIC.  First I discovered that CoGetClassObject() wasn't declared in PowerBASIC's Win32Api.inc file along with CoCreateInstance.  In fact, it didn't even seem to be in any file in the \Include directory of PowerBASIC.  So I looked in Jose's includes and found it there.  It really is an important function.  To use it though we're going to have to create another function pointer declare such as we have with QueryInterface, AddRef, and release.  There are some other issues too.  Note in the above C++ code I passed in a NULL for the third parameter.  That won't work with Jose's declare I don't believe so we'll need to pass in what the function really wants and that is a pointer to a  COSERVERINFO structure.   Lucky for us Jose translated that in his includes.  Perhaps I'd better display the Api docs on CoGetClassObject()...

edit - 1/20/2008 @10:09 PM  -Jose told me to just use Byval %NULL for the Null COSERVERINFO pointer.  It works!


STDAPI CoGetClassObject
(
  REFCLSID       rclsid,         //CLSID associated with the class object
  DWORD          dwClsContext,   //Context for running executable code
  COSERVERINFO*  pServerInfo,    //Pointer to machine on which the object is to be instantiated
  REFIID         riid,           //Reference to the identifier of the interface
  LPVOID*        ppv             //Address of output variable that receives the interface pointer requested in riid
);


     Also, we need to define the IID for the IClassFactory interface and that is {00000001-0000-0000-C000-000000000046}.  Here is the necessary equate... 

$IID_IClassFactory =Guid$("{00000001-0000-0000-C000-000000000046}")  'Microsoft Defined - IClassFactory     

     Finally, we need to construct a suitable function pointer declare to call CAClassFactory::CreateInstance() once CoGetClassObject() returns a class factory pointer to us.  I've found this to work...

Declare Function ptrCreateInstance (Byval this As Dword, ByVal pUnknown As Dword, Byref iid As Guid, Byref pVTbl As Any) As Long     

     Note that we need to pass in the required this pointer so as not to unbalance the stack.  Below is the PowerBASIC program using CoGetClassObject().  You need to use Jose's includes for this.


#Compile               Exe "CAClient.exe"
#Dim                   All
#Include               "Win32Api.inc"
#Include               "ObjBase.inc"
%CLSCTX_INPROC_SERVER  =&H1???
$IID_IUnknown          =Guid$("{00000000-0000-0000-C000-000000000046}")  'Microsoft Defined - IUnknown
$IID_IClassFactory     =Guid$("{00000001-0000-0000-C000-000000000046}")  'Microsoft Defined - IClassFactory
$CLSID_CA              =Guid$("{20000000-0000-0000-0000-000000000004}")  'Class ID of Class CA, ie., Class A
$IID_IX                =Guid$("{20000000-0000-0000-0000-000000000005}")  'Interface X
$IID_IY                =Guid$("{20000000-0000-0000-0000-000000000006}")  'Interface Y
$IID_Junk              =Guid$("{12345678-9876-5432-1012-345678901234}")  'Junk Number So QueryInterface() Fails

Declare Function ptrCreateInstance (Byval this As Dword, Byval pUnknown As DWord, Byref iid As Guid, Byref pVTbl As Any) As Long
Declare Function ptrQueryInterface (Byval this As Dword, Byref iid As Guid, Byref pUnknown As Any) 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,pCF,CFVTbl,pUnk As Dword Ptr
  Local hResult As Long
  Register i As Long

  hResult=CoGetClassObject($CLSID_CA, %CLSCTX_INPROC_SERVER, Byval %NULL, $IID_IClassFactory, pCF)
  If SUCCEEDED(hResult) Then
     Print "CoGetClassObject() Succeeded!"
     Print "pCF         = " pCF
     CFVTbl=@pCF[0]  '@CFVTbl[3] is the address of CAClassFactory::CreateInstance(...)
     Call Dword @CFVTbl[3] Using ptrCreateInstance(pCF,pUnk,$IID_IUnknown,pVTbl) To hResult
     If SUCCEEDED(hResult) Then  'If we get inside this If the class factory has alreadt created class CA
        Print "pCF->CreateInstance() Succeeded!"  'so we can do a pCF->Release().  Release() is in slot #2
        Call DWord @CFVTbl[2] Using ptrRelease(pCF) To hResult  'Release() CAClassFactory
        Print "pVTbl       = " pVTbl  'We now have the same VTbl Ptr ( pVTbl ) we originally got from
        Print                         'CoCreateInstance(), so go to town.....
        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(pVTbl) To hResult
     End If
  End If
  Waitkey$

  PBMain=0
End Function

'Called CAClassFactory::AddRef()
'Called CAClassFactory::AddRef()
'Called CAClassFactory::Release()
'Called CAClassFactory::AddRef()
'Called CAClassFactory::Release()
'CoGetClassObject() Succeeded!
'pCF         =  14484944
'this        = 14484880
'sizeof(CA)  = 12
'Called CA::AddRef()
'pCF->CreateInstance() Succeeded!
'Called CAClassFactory::Release()
'pVTbl       =  14484880
'
'Varptr(@pVTbl[i])  Varptr(@VTbl[i])  @VTbl[i]     Function Call With Call Dword
'===============================================================================
'14484880           14381324          14356624    Called CA::QueryInterface()
'14484880           14381328          14356768    Called CA::AddRef()
'14484880           14381332          14356800    Called CA::Release()
'14484880           14381336          14356864    Called Fx1()  :  iNum = 0
'14484880           14381340          14356896    Called Fx2()  :  iNum = 0
'
'14484884           14381304          14357392    Called CA::QueryInterface()
'14484884           14381308          14357408    Called CA::AddRef()
'14484884           14381312          14357424    Called CA::Release()
'14484884           14381316          14356928    Called Fy1()  :  iNum = 1
'14484884           14381320          14356960    Called Fy2()  :  iNum = 1
'
'Called CA::Release()


     Well, there you have it.  I'd like to continue and show how we can do away with system COM services entirely and just load the COM class in the dll ourselves with LoadLibrary() and GetProcAddress(), but I believe I'll leave that for later and just provide some closing thoughts. 

     It is certainly not necessary to go through all this that I have done here to access a COM object.  It is interesting to compare minimal programs side by side in PowerBASIC and C++ that access the I_X and I_Y interfaces of ComObject.CA that we have been examining under a microscope.  Here is a minimal PowerBASIC program with output followed by the exact same thing in C++...
   

#Compile               Exe "CAClient.exe"
#Dim                   All
#Include               "Win32Api.inc"
'#Include               "ObjBase.inc"
$CLSID_CA              =Guid$("{20000000-0000-0000-0000-000000000004}")  'Class ID of Class CA, ie., Class A
$IID_IX                =Guid$("{20000000-0000-0000-0000-000000000005}")  'Interface X
$IID_IY                =Guid$("{20000000-0000-0000-0000-000000000006}")  'Interface Y


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                'Important Note!  I had originally made
  Local hResult As Long                  'a mistake in my original posting of this
  Local ix As I_X                        'little program that Jose caught!  In
  Local iy As I_Y                        'order to call the I_Y interface functions...

  ix=NewCom Clsid $CLSID_CA              'I had done this > iy=NewCom("ComObject.CA")
  hResult=ix.Fx1(25)                     'That isn't necessary!  PowerBASIC does,
  hResult=ix.Fx2(50)                     'behind the scenes, so to speak, a
  iy=ix   ''iy=NewCom("ComObject.CA")    'QueryInterface() on I_Y using I_X exactly
  hResult=iy.Fy1(75)                     'as I had done in the C++ program below!
  hResult=iy.Fy2(100)                    'This is actually one of the fundamental
  Set ix = Nothing                       'rules of COM.  If you have an interface
  Set iy = Nothing                       'on an object you should be able to get
  Waitkey$                               'any other interface (if you have its IID).

  PBMain=0
End Function


'===Output==================
'Called CA::AddRef()
'Called CA::AddRef()
'Called CA::Release()
'Called CA::AddRef()
'Called CA::Release()
'Called Fx1()  :  iNum = 25
'Called Fx2()  :  iNum = 50
'Called CA::AddRef()
'Called Fy1()  :  iNum = 75
'Called Fy2()  :  iNum = 100
'Called CA::Release()
'Called CA::Release()



//C++ Version

#include <windows.h>            //CAClient6
#include <stdio.h>
static const CLSID CLSID_CA   = {0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04}};
static const IID   IID_I_X    = {0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05}};
static const IID   IID_I_Y    = {0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06}};


interface I_X : IUnknown
{
virtual HRESULT __stdcall Fx1(int)=0;
virtual HRESULT __stdcall Fx2(int)=0;
};


interface I_Y : IUnknown
{
virtual HRESULT __stdcall Fy1(int)=0;
virtual HRESULT __stdcall Fy2(int)=0;
};


int main(void)
{
I_X* pIX=NULL;
I_Y* pIY=NULL;
HRESULT hr;

hr=CoInitialize(NULL);
hr=CoCreateInstance(CLSID_CA, NULL, CLSCTX_INPROC_SERVER, IID_I_X, (void**)&pIX);
hr=pIX->Fx1(25);
hr=pIX->Fx2(50);
hr=pIX->QueryInterface(IID_I_Y,(void**)&pIY);
hr=pIY->Fy1(75);
hr=pIY->Fy2(100);
hr=pIX->Release();
hr=pIY->Release();
CoUninitialize();

return 0;
}


/*===Output=================
sizeof(CA) = 12
this       = 10355696
Called CA::AddRef()
Called CA::AddRef()
Called CA::Release()
Called CA::AddRef()
Called CA::Release()
Called Fx1()  :  iNum = 25
Called Fx2()  :  iNum = 50
Called CA::AddRef()
Called Fy1()  :  iNum = 75
Called Fy2()  :  iNum = 100
Called CA::Release()
Called CA::Release()
*/


     They are almost the same exact number of lines even!  Interestingly, in the PowerBASIC program class CA apparently had to be created twice, as that is what the output shows, whereas in the C++ program I just did a QueryInterface() for I_Y off of the I_X interface I already had.  Upon examining this issue in the PowerBASIC documentation it seems to state I could have used GetCom instead of AnyCom or NewCom if I had implemented the IDispatch interface in the object, but as you would know if you examined my C++ code for the COM object at all, the IDispatch interface wasn't implemented.  I'm just mentioning this as a curiosity and don't consider it of any major import – at least not to me.

     Finally, I'd like to address the question of what I got out of this exceedingly long endeavor.  Well, if nothing else I've an exceedingly good idea now of what is happening behind the scenes in C++ code when I see something like this...


virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv)
{
  if(iid==IID_IX)
     *ppv=static_cast<IX*>(this);   
  else if(iid==IID_IY)
     *ppv=static_cast<IY*>(this);   
  else if(iid==IID_IUnknown)
     *ppv=static_cast<IX*>(this);
  else
  {
     *ppv=NULL;
     return E_NOINTERFACE;
  }
  static_cast<IUnknown*>(*ppv)->AddRef();
 
  return S_OK;
}


(The exceedingly good idea I have about it is that the pointer stored in ppv is being set to the address of the requested Vtable).

Frederick J. Harris

Jose found some mistakes.  >:(

But I fixed them already!  :)

By the way, I made some comments regarding my difficulties creating the C++ COM dll with the GNU compilers that are used by the Dev-C++ and new CodeBlocks development systems.  This comment doesn't apply to the client programs at all.  There is no problem whatsoever with that.  If you register the dll with regsvr32 you'll be able to use those GNU compilers for the client programs. 

I might point out I probably am not following the best practices in the way I associated GUIDs to the interfaces.  I didn't use GuidGen or any of the other tools.  I just made up the numbers 20000000-0000-0000-0000-00000004, 20000000-0000-0000-0000-00000005, and 20000000-0000-0000-0000-00000006.  So it probably wouldn't hurt to check your registry that those numbers don't overwrite anything (You need to expand your CLSID key and search through your millions of numbers.  They are in ascending order.  If you don't know what I'm talking about you need to learn that before you start learning COM - only a few months ago I didn't know it).

James C. Fuller

 Frederick,
  Excellent stuff. I had just started investigating accessing PowerBASIC created COM servers using c or c++.
I have not delved too deeply into your toutorial yet (but I will).
Does there exist a utility similar to the PB com browsers  that will create an include file from a typelib for c/c++?

James

James C. Fuller

Fred,
  Just a heads up.
  I used Borland's free c++ compiler 5.5 with RADasm and the client1 cpp code worked flawlessly.

James

Frederick J. Harris

#6
Hi James

     Thanks for the feedback.  I have a good bit of code written to create procedure declarations for C/C++.  I was doing it just to learn the whole TypeLib territory working with the interfaces, etc.  I figured there probably were a multitude of tools out there to do it from the C/C++ angle.

     When I get time I'm going to explore further the difficulties I was having with the GNU compilers.  I really see no reason why they won't work.  Actually, they sometimes do work creating the COM object.  But I have to work on 'real' work right now!

     Anyway, that's something I'd like to work on sometime if I have a little time, i.e., a TypeBib Browser with a radio button that produces either PB or C++ includes.

James C. Fuller

Fred,
  I know you mentioned covering this in Part 2 but would it be possible to get the c++ code to load
an unregistered com object (using your CA.DLL example) from a dll? My c++ knowledge is very poor at best.

This is the José PB code I used before the functionality was added to the compiler.

Thank You,

James


' ========================================================================================
' Loads the specified library and creates an instance of an object.
' If it succeeds, returns a reference to the requested interface; otherwise, it returns null.
' Note: Do not use FreeLibrary to unload the library once you have got a valid reference
' to an interface or your application will GPF. Before calling FreeLibrary, all the
' interface references must be released. If you don't need to unload the library until
' the application ends, then you don't need to call FreeLibrary because PB calls
' CoUninitialize under the hood, that closes the COM library on the current thread,
' unloads all DLLs loaded by the thread, frees any other resources that the thread
' maintains, and forces all RPC connections on the thread to close.
' ========================================================================================
FUNCTION CreateInstanceFromDll ( _
   BYVAL strLibName AS STRING _               ' // [in] Library name and path
, BYREF rclsid AS GUID _                     ' // [in] CLSID that will associate the correct data and code
, BYREF riid AS GUID _                       ' // [in] IID of the interface to be used to communicate with the newly created objec
, OPTIONAL BYVAL strLicKey AS STRING _       ' // [in] License key (ansi string)
) AS IUnknown                                ' // [out] The interface pointer requested in riid

   LOCAL hr AS LONG                           ' // HRESULT
   LOCAL hLib AS DWORD                        ' // Library handle
   LOCAL pProc AS DWORD                       ' // Procedure address
   LOCAL pIClassFactory AS IClassFactory      ' // IClassFactory interface
   LOCAL pIClassFactory2 AS IClassFactory2    ' // IClassFactory2 interface
   LOCAL ppv AS IUnknown                      ' // The requested interface pointer

   ' // See if the library is already loaded in the address space
   hLib = GetModuleHandle(BYCOPY strLibName)
   ' // If it is not loaded, load it
   IF hLib = %NULL THEN hLib = LoadLibrary(BYCOPY strLibName)
   ' // If it fails, abort
   IF ISFALSE hLib THEN EXIT FUNCTION

   ' // Retrieve the address of the exported function DllGetClassObject
   pProc = GetProcAddress(hLib, "DllGetClassObject")
   IF ISFALSE pProc THEN
      FreeLibrary hLib
      EXIT FUNCTION
   END IF

   IF LEN(strLicKey) = 0 THEN
      ' // Request a reference to the IClassFactory interface
      CALL DWORD pProc USING DllGetClassObject(rclsid, $IID_IClassFactory, pIClassFactory) TO hr
      IF hr <> %S_OK THEN
         FreeLibrary hLib
         EXIT FUNCTION
      END IF
      ' // Create an instance of the server or control
      hr = pIClassFactory.CreateInstance(%NULL, riid, BYVAL VARPTR(ppv))
      IF hr <> %S_OK THEN EXIT FUNCTION
   ELSE
      ' // Request a reference to the IClassFactory2 interface
      CALL DWORD pProc USING DllGetClassObject(rclsid, $IID_IClassFactory2, pIClassFactory2) TO hr
      IF hr <> %S_OK THEN
         FreeLibrary hLib
         EXIT FUNCTION
      END IF
      ' // Create a licensed instance of the server or control
      strLicKey = UCODE$(strLicKey)
      hr = pIClassFactory2.CreateInstanceLic(NOTHING, NOTHING, riid, strLicKey, ppv)
      IF hr <> %S_OK THEN EXIT FUNCTION
   END IF

   FUNCTION = ppv

END FUNCTION

Frederick J. Harris

#8
My confession: Never even tried it!  Just assummed it would work.  Just banged this out and luckily it does!

(you need to change the path to where you have CA.dll in LoadLibrary() call at top)

//CAClient3.cpp
#include <windows.h> //CAClient
#include <stdio.h>   //IID_IClassFactory={00000001-0000-0000-C000-000000000046};
const CLSID CLSID_CA ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04}}; //Class ID of Class CA, ie., Class A
const IID   IID_I_X  ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05}}; //Interface X
const IID   IID_I_Y  ={0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06}}; //Interface Y
const IID   IID_Junk ={0x12345678,0x9876,0x5432,{0x12,0x34,0x56,0x78,0x98,0x76,0x54,0x32}}; //Junk Number So QueryInterface() Fails

HRESULT (__stdcall* ptrDllGetClassObject) (const CLSID&, const IID&, void**);
HRESULT (__stdcall* ptrQueryInterface)    (int, const IID&, void**);
ULONG   (__stdcall* ptrAddRef)            (int);
ULONG   (__stdcall* ptrRelease)           (int);
void    (__stdcall* pIFn)                 (int,int);


interface I_X : IUnknown
{
virtual HRESULT __stdcall Fx1(int)=0;
virtual HRESULT __stdcall Fx2(int)=0;
};


interface I_Y : IUnknown
{
virtual HRESULT __stdcall Fy1(int)=0;
virtual HRESULT __stdcall Fy2(int)=0;
};


int main(void)
{
IClassFactory*  pCF=NULL;
HMODULE         hDll=NULL;
BOOL blnFree    =FALSE;
IUnknown*       pIUnk=NULL;
unsigned int*   pVTbl=0;
unsigned int*   VTbl=0;
HRESULT         hr;

hDll=LoadLibrary("C:\\Code\\VStudio\\VC++6\\Projects\\COM\\ComObjects\\CA\\Release\\CA.dll");
printf("hDll=%u\n",hDll);
if(hDll)
{
    ptrDllGetClassObject=(HRESULT (__stdcall*)(REFCLSID, REFIID, void**))GetProcAddress(hDll,"DllGetClassObject");
    printf("ptrDllGetClassObject = %u\n",ptrDllGetClassObject);
    hr=ptrDllGetClassObject(CLSID_CA,IID_IClassFactory,(void**)&pCF);
    if(SUCCEEDED(hr))
    {
       puts("Worked!");
       pCF->CreateInstance(NULL,IID_IUnknown,(void**)&pVTbl);
       pCF->Release();
       printf("pVTbl   = %u\n",pVTbl);
       printf("\n");
       printf("&pVTbl[i]\t&VTbl[i]\tVTbl[i]\t\tFunction Call Through Pointer\n");
       printf("=============================================================================\n");
       for(unsigned int i=0;i<=1;i++)
       {
           VTbl=(unsigned int*)pVTbl[i];                                             //Call...
           printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[0],VTbl[0]);   
           ptrQueryInterface=(HRESULT(__stdcall*)(int, const IID&, void**)) VTbl[0];
           ptrQueryInterface((int)&pVTbl[i],IID_Junk,(void**)&pIUnk);                //QueryInterface()
           printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[1],VTbl[1]);                       
           ptrAddRef=(ULONG(__stdcall*)(int)) VTbl[1];
           ptrAddRef((int)&pVTbl[i]);                                                //AddRef()
           printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[2],VTbl[2]);                       
           ptrRelease=(ULONG(__stdcall*)(int)) VTbl[2];
           ptrRelease((int)&pVTbl[i]);                                               //Release()
           printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[3],VTbl[3]);                       
           pIFn=(void(__stdcall*)(int,int)) VTbl[3];
           pIFn((int)&pVTbl[i],i);                                                   //Fx1() / Fy1()   
           printf("%u\t%u\t%u\t",&pVTbl[i],&VTbl[4],VTbl[4]);                       
           pIFn=(void(__stdcall*)(int,int)) VTbl[4];
           pIFn((int)&pVTbl[i],i);                                                   //Fx2() / Fy2()   
           printf("\n");
       }
       ptrRelease=(ULONG(__stdcall*)(int)) VTbl[2];
       ptrRelease((int)&pVTbl[0]);
       getchar();
    }   
    blnFree=FreeLibrary(hDll);
    printf("blnFree=%u\n",blnFree);
}

return 0;
}


Let me know if that works Jim!  You had mentioned you had ver 5.5 of Borland's C++ product.  I actually have the standard edition version 5.  I almost never used it, as right after that I obtained Microsoft's high octane version through my employer, and I'm still using that. 

I've had a love/hate relationship with C++ for years.  After I struggled teaching myself C way back in the nineties I was looking forward to learning C++.  When I started learning about classes and all that stuff I thought it was really neat.  Then I bumbed up hard against MFC and that stoped me dead in my tracks.  I truely hated it.  I just simply don't like class libraries that wrap the Windows Api.  At the time though I wasn't prescient enough to make the distinction between C++ and class libraries that wrap the Windows Api; all I knew was that I hated MFC and C++ was associated with it rather intimately, and I more or less threw out the baby with the bathwater.  As time went on though it dawned on me that the type of raw Windows Api programming I do with PowerBASIC I could do just as well in C++, the only real problem being the lack of dynamic string handling abilities in native unadorned C++.  Yes, C++ has string class libraries, but as far as I know you need to link with the horrible big and bloated MFC.dll to use them, and under no circumstances was I ever about to do that.  So eventually I got good enough with it to write my own string class, and I use that now in C++ apps.  So really, I've the best of both worlds, i.e., a super powerful OOP implementation, plus I can write small executables just like in PowerBASIC.

At work I pretty much have to use C++ for my data recorder work but I only use PowerBASIC for desktop work - not much C++ except for learning this COM stuff.

....back to the program though...most C++ folks (all C++ folks!?!) use typedefs to cast the return from GetProcAddress() to a usable function pointer.  I don't usually (I'll be honest - I never do it) do that because typedefs confuse me more than they help me.  The way I see the issue is that if I create a typedef it cleans up the point of call, but it introduces 'magic' in my programs.  If I don't use a typedef its ugly as sin at the point of call but at least there's no witchcraft involved! 

Frederick J. Harris

I added a bunch of printf statements within the dll, recompiled it, then ran this PowerBASIC program that uses GetProcAddress() instead of COM services to load the dll.  It seems to work OK. 


#Compile               Exe "CAClient3.exe"
#Dim                   All
#Include               "Win32Api.inc"
#Include               "ObjBase.inc"
%CLSCTX_INPROC_SERVER  =&H1???
$IID_IUnknown          =Guid$("{00000000-0000-0000-C000-000000000046}")  'Microsoft Defined - IUnknown
$IID_IClassFactory     =Guid$("{00000001-0000-0000-C000-000000000046}")  'Microsoft Defined - IClassFactory
$CLSID_CA              =Guid$("{20000000-0000-0000-0000-000000000004}")  'Class ID of Class CA, ie., Class A
$IID_IX                =Guid$("{20000000-0000-0000-0000-000000000005}")  'Interface X
$IID_IY                =Guid$("{20000000-0000-0000-0000-000000000006}")  'Interface Y
$IID_Junk              =Guid$("{12345678-9876-5432-1012-345678901234}")  'Junk Number So QueryInterface() Fails

Declare Function DllGetClassObject   (Byref Clsid As Guid, Byref iid As Guid, Byref pIUnknown As Any) As Long
Declare Function ptrCreateInstance   (Byval this As Dword, Byref pUnknown As Any, Byref iid As Guid, Byref pVTbl As Any) As Long
Declare Function ptrQueryInterface   (Byval this As Dword, Byref iid As Guid, Byref pUnknown As Any) 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,pCF,CFVTbl,pUnk,pDllProc As Dword Ptr
  Local pSerInfo As COSERVERINFO
  Local hResult As Long
  Local hDll As DWord
  Register i As Long

  hDll=LoadLibrary("C:\Code\VStudio\VC++6\Projects\COM\ComObjects\CA\Release\CA.dll")    'Change This Line!!!!
  'hDll=LoadLibrary("C:\Code\DevC++\CA\CA.dll")
  If hDll Then
     Print "hDll     = " hDll
     pDllProc=GetProcAddress(hDll,"DllGetClassObject")
     If pDllProc Then
        Print "pDllProc = " pDllProc
        Call DWord pDllProc Using DllGetClassObject($CLSID_CA, $IID_IClassFactory, pCF) To hResult
        'hResult=CoGetClassObject($CLSID_CA, %CLSCTX_INPROC_SERVER, pSerInfo, $IID_IClassFactory, pCF)
        If SUCCEEDED(hResult) Then
           Print "Successfully Loaded CA.dll And Succeeded In DllGetClassObject() Call!"
           Print "pCF      = " pCF
           CFVTbl=@pCF[0]  '@CFVTbl[3] is the address of CAClassFactory::CreateInstance(...)
           Call Dword @CFVTbl[3] Using ptrCreateInstance(pCF,pUnk,$IID_IUnknown,pVTbl) To hResult
           If SUCCEEDED(hResult) Then  'If we get inside this If the class factory has alreadt created class CA
              Print "pCF->CreateInstance() Succeeded!"  'so we can do a pCF->Release().  Release() is in slot #2
              Call DWord @CFVTbl[2] Using ptrRelease(pCF) To hResult  'Release() CAClassFactory
              Print "pVTbl    = " pVTbl  'We now have the same VTbl Ptr ( pVTbl ) we originally got from
              Print                         'CoCreateInstance(), so go to town.....
              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(pVTbl) To hResult
           End If
        End If
        FreeLibrary(hDll)
     End If
  End If
  Waitkey$

  PBMain=0
End Function

'hDll     =  268435456
'pDllProc =  268441664
'
'Entering DllGetClassObject()
'Called CAClassFactory Constructor!
'Called CAClassFactory::QueryInterface()
'Called CAClassFactory::AddRef()
'Leaving DllGetClassObject()
'
'Successfully Loaded CA.dll And Succeeded In DllGetClassObject() Call!
'pCF      =  8521248
'Called CAClassFactory::CreateInstance()
'this       = 8521216
'sizeof(CA) = 12
'Called CA::AddRef()
'pCF->CreateInstance() Succeeded!
'Called CAClassFactory::Release()
'Called CAClassFactory Destructor!
'pVTbl    =  8521216
'
'Varptr(@pVTbl[i])  Varptr(@VTbl[i])  @VTbl[i]     Function Call With Call Dword
'===============================================================================
'8521216            268464396         268439696    Called CA::QueryInterface()
'8521216            268464400         268439840    Called CA::AddRef()
'8521216            268464404         268439872    Called CA::Release()
'8521216            268464408         268439936    Called Fx1()  :  iNum = 0
'8521216            268464412         268439968    Called Fx2()  :  iNum = 0
'
'8521220            268464376         268440528    Called CA::QueryInterface()
'8521220            268464380         268440544    Called CA::AddRef()
'8521220            268464384         268440560    Called CA::Release()
'8521220            268464388         268440000    Called Fy1()  :  iNum = 1
'8521220            268464392         268440032    Called Fy2()  :  iNum = 1
'
'Called CA::Release()
                             

James C. Fuller

I modified your c++ Client6 code and it works. I'm not sure if it's leaking or not though.

James



#include <windows.h>            //CAClient6
#include <stdio.h>
static const CLSID CLSID_CA   = {0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04}};
static const IID   IID_I_X    = {0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05}};
static const IID   IID_I_Y    = {0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06}};

HRESULT (__stdcall* ptrDllGetClassObject) (const CLSID&, const IID&, void**);

interface I_X : IUnknown
{
virtual HRESULT __stdcall Fx1(int)=0;
virtual HRESULT __stdcall Fx2(int)=0;
};


interface I_Y : IUnknown
{
virtual HRESULT __stdcall Fy1(int)=0;
virtual HRESULT __stdcall Fy2(int)=0;
};


int main(void)
{
  I_X* pIX=NULL;
  I_Y* pIY=NULL;
  HRESULT hr;
  IClassFactory*  pCF=NULL;
  HMODULE         hDll=NULL;
  BOOL blnFree    =FALSE;

  hr=CoInitialize(NULL);
  hDll=LoadLibrary("D:\\ComTutorial1\\Server\\bin\\CA.dll");
  ptrDllGetClassObject=(HRESULT (__stdcall*)(REFCLSID, REFIID, void**))GetProcAddress(hDll,"DllGetClassObject");
  printf("ptrDllGetClassObject = %u\n",ptrDllGetClassObject);
  hr=ptrDllGetClassObject(CLSID_CA,IID_IClassFactory,(void**)&pCF);
  if(SUCCEEDED(hr))
  {
    pCF->CreateInstance(NULL,IID_I_X,(void**)&pIX);
    // is this needed ??
    pCF->Release();
    hr=pIX->Fx1(25);
    hr=pIX->Fx2(50);
    hr=pIX->QueryInterface(IID_I_Y,(void**)&pIY);
    hr=pIY->Fy1(75);
    hr=pIY->Fy2(100);
    hr=pIX->Release();
    hr=pIY->Release();
  }
CoUninitialize();
blnFree=FreeLibrary(hDll);
getchar();
return 0;

}





Frederick J. Harris

Yes, the pCF->Release() is needed because CAClassFactory::QueryInterface() increments the reference count when it gets called.  If a Release() isn't called then I think memory would leak.  What I don't believe you need are the CoInitialize() and CoUninitialize() as we aren't using Windows COM Services (SCM - service control manager) to do  anything for us. 

As I had mentioned, I added a bunch of printf statements to the dll and recompiled.  Using this version and running your program with and without the pCF->Release() yields the following results.  Notice the CAClassFactory destructor isn't called without the Release() in the second run...


#include <windows.h>            //CAClient9
#include <stdio.h>
static const CLSID CLSID_CA   = {0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x04}};
static const IID   IID_I_X    = {0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x05}};
static const IID   IID_I_Y    = {0x20000000,0x0000,0x0000,{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x06}};

HRESULT (__stdcall* ptrDllGetClassObject) (const CLSID&, const IID&, void**);

interface I_X : IUnknown
{
virtual HRESULT __stdcall Fx1(int)=0;
virtual HRESULT __stdcall Fx2(int)=0;
};


interface I_Y : IUnknown
{
virtual HRESULT __stdcall Fy1(int)=0;
virtual HRESULT __stdcall Fy2(int)=0;
};


int main(void)
{
  I_X* pIX=NULL;
  I_Y* pIY=NULL;
  HRESULT hr;
  IClassFactory*  pCF=NULL;
  HMODULE         hDll=NULL;
 
  hDll=LoadLibrary("C:\\Code\\VStudio\\VC++6\\Projects\\COM\\ComObjects\\CA\\Release\\CA.dll");
  //hDll=LoadLibrary("C:\\Code\\VStudio\\CA\\Release\\CA.dll");
  ptrDllGetClassObject=(HRESULT (__stdcall*)(REFCLSID, REFIID, void**))GetProcAddress(hDll,"DllGetClassObject");
  printf("ptrDllGetClassObject = %u\n",ptrDllGetClassObject);
  hr=ptrDllGetClassObject(CLSID_CA,IID_IClassFactory,(void**)&pCF);
  if(SUCCEEDED(hr))
  {
    pCF->CreateInstance(NULL,IID_I_X,(void**)&pIX);
    // is this needed ??
    pCF->Release();
    hr=pIX->Fx1(25);
    hr=pIX->Fx2(50);
    hr=pIX->QueryInterface(IID_I_Y,(void**)&pIY);
    hr=pIY->Fy1(75);
    hr=pIY->Fy2(100);
    hr=pIX->Release();
    hr=pIY->Release();
  }
FreeLibrary(hDll);
getchar();

return 0;
}

/*
====Output Including pCF->Release=======
========================================
ptrDllGetClassObject = 268441664

Entering DllGetClassObject()
Called CAClassFactory Constructor!
Called CAClassFactory::QueryInterface()
Called CAClassFactory::AddRef()
Leaving DllGetClassObject()

Called CAClassFactory::CreateInstance()
this       = 10421168
sizeof(CA) = 12
Called CA::AddRef()
Called CAClassFactory::Release()
Called CAClassFactory Destructor!
Called Fx1()  :  iNum = 25
Called Fx2()  :  iNum = 50
Called CA::AddRef()
Called Fy1()  :  iNum = 75
Called Fy2()  :  iNum = 100
Called CA::Release()
Called CA::Release()
*/

/*
====Output With No pCF->Release()========
=========================================
ptrDllGetClassObject = 268441664

Entering DllGetClassObject()
Called CAClassFactory Constructor!
Called CAClassFactory::QueryInterface()
Called CAClassFactory::AddRef()
Leaving DllGetClassObject()

Called CAClassFactory::CreateInstance()
this       = 10421168
sizeof(CA) = 12
Called CA::AddRef()
Called Fx1()  :  iNum = 25
Called Fx2()  :  iNum = 50
Called CA::AddRef()
Called Fy1()  :  iNum = 75
Called Fy2()  :  iNum = 100
Called CA::Release()
Called CA::Release()
*/

Frederick J. Harris

#12
     So COM objects are created in two steps (three, if you want to include calling DllGetClassObject()).  First, the class factory is created through some kind of memory allocation - in a C++ program probably through the 'new' operator.  At that time there will be a QueryInterface call that will increment the reference count on the class factory.  Then, once the class factory is created, the client can call CAClassFactory::CreateInstance() to create the container class - in this case CA.  At that point there will be another memory allocation (actually, quite a few of them) for that object.  And there will still be two objects in memory - CA and CA's ClassFactory.  If you hsve no further need of creating additional CAs, then the class factory should be released.  In the Release() call the counter member is checked and if it is zero the delete operator is called and that causes the CAClassFactory destructor to execute.   

     .....but, I just thought of something cool.  Will give it a try....

James C. Fuller

#13
Well I thought I'd give this a try.
Created the server with PB
I used GUIDGEN.

James

PbServer Code:

'SED_PBWIN
#COMPILE DLL "PBCA.DLL"
#DIM ALL
#INCLUDE ONCE "Windows.inc"
$CLSID_CA =GUID$("{AB2D3628-5A59-4719-970B-1D6D70761AE8}")
'0xab2d3628, 0x5a59, 0x4719, 0x97, 0xb, 0x1d, 0x6d, 0x70, 0x76, 0x1a, 0xe8);

$IID_I_X = GUID$("{77E3ACD0-E12B-428c-A8E8-E32526F14DD4}")
'0x77e3acd0, 0xe12b, 0x428c, 0xa8, 0xe8, 0xe3, 0x25, 0x26, 0xf1, 0x4d, 0xd4);

$IID_I_Y = GUID$("{68DDC584-EC29-4214-A907-0A3FAD7D7E32}")
'0x68ddc584, 0xec29, 0x4214, 0xa9, 0x7, 0xa, 0x3f, 0xad, 0x7d, 0x7e, 0x32);


CLASS CA $CLSID_CA AS COM
CLASS METHOD CREATE
STDOUT "Constructor Called"
END METHOD
CLASS METHOD DESTROY
STDOUT "Destructor Called"
END METHOD
INTERFACE I_X $IID_I_X : INHERIT IUNKNOWN
METHOD Fx1(BYVAL iNum AS LONG)
STDOUT "Called Fx1 iNum = " + FORMAT$(iNum) 
END METHOD
METHOD Fx2(BYVAL iNum AS LONG)
STDOUT "Called Fx2 iNum = " + FORMAT$(iNum)
END METHOD
END INTERFACE
INTERFACE I_Y $IID_I_Y : INHERIT IUNKNOWN
METHOD Fy1(BYVAL iNum AS LONG)
STDOUT "Called Fy1 iNum = " + FORMAT$(iNum) 
END METHOD
METHOD Fy2(BYVAL iNum AS LONG)
STDOUT "Called Fy2 iNum = " + FORMAT$(iNum) 
END METHOD
END INTERFACE
END CLASS
'==============================================================================
FUNCTION STDOUT (Z AS STRING) AS LONG
' returns TRUE (non-zero) on success

   LOCAL hStdOut AS LONG, nCharsWritten AS LONG
   LOCAL w AS STRING
   

   hStdOut      = GetStdHandle (%STD_OUTPUT_HANDLE)
   IF hSTdOut   = -1&  or hStdOut = 0&  THEN     ' invalid handle value, coded in line to avoid 
                                                 ' casting differences in Win32API.INC
                                                 ' %NULL test added for Win/XP
     AllocConsole
     hStdOut  = GetStdHandle (%STD_OUTPUT_HANDLE)
   END IF
   w = Z & $CRLF
   FUNCTION = WriteFile(hStdOut, BYVAL STRPTR(W), LEN(W),  nCharsWritten, BYVAL %NULL)
   

END FUNCTION
'==============================================================================


c++ client code


#include <windows.h>           
#include <stdio.h>
static const CLSID CLSID_CA   = {0xab2d3628, 0x5a59, 0x4719,{ 0x97, 0xb, 0x1d, 0x6d, 0x70, 0x76, 0x1a, 0xe8}};
static const IID   IID_I_X    = {0x77e3acd0, 0xe12b, 0x428c,{ 0xa8, 0xe8, 0xe3, 0x25, 0x26, 0xf1, 0x4d, 0xd4}};
static const IID   IID_I_Y    = {0x68ddc584, 0xec29, 0x4214,{0xa9, 0x7, 0xa, 0x3f, 0xad, 0x7d, 0x7e, 0x32}};

HRESULT (__stdcall* ptrDllGetClassObject) (const CLSID&, const IID&, void**);

interface I_X : IUnknown
{
virtual HRESULT __stdcall Fx1(int)=0;
virtual HRESULT __stdcall Fx2(int)=0;
};


interface I_Y : IUnknown
{
virtual HRESULT __stdcall Fy1(int)=0;
virtual HRESULT __stdcall Fy2(int)=0;
};


int main(void)
{
  I_X* pIX=NULL;
  I_Y* pIY=NULL;
  HRESULT hr;
  IClassFactory*  pCF=NULL;
  HMODULE         hDll=NULL;
  BOOL blnFree    =FALSE;



//Change Path Here

  hDll=LoadLibrary("D:\\ComTutorial1A\\Server\\PbSource\\PBCA.DLL");
 
  ptrDllGetClassObject=(HRESULT (__stdcall*)(REFCLSID, REFIID, void**))GetProcAddress(hDll,"DllGetClassObject");
  printf("ptrDllGetClassObject = %u\n",ptrDllGetClassObject);
  hr=ptrDllGetClassObject(CLSID_CA,IID_IClassFactory,(void**)&pCF);
  if(SUCCEEDED(hr))
  {
    pCF->CreateInstance(NULL,IID_I_X,(void**)&pIX);
    // is this needed ??
    pCF->Release();
    hr=pIX->Fx1(25);
    hr=pIX->Fx2(50);
    hr=pIX->QueryInterface(IID_I_Y,(void**)&pIY);
    hr=pIY->Fy1(75);
    hr=pIY->Fy2(100);
    hr=pIX->Release();
    hr=pIY->Release();
  }

blnFree=FreeLibrary(hDll);
getchar();
return 0;

}




Frederick J. Harris

Hi Jim!

     Fun stuff, Huh?  Its near my bed time so I don't expect I'll give your latest a try 'till tomorrow, but its my guess that if you take the memory apart and examine it you'll find out that PB arranges the VTable pointers somewhat differently.  I do believe there are parts of this COM stuff that Microsoft has more or less etched in stone, and other parts that are not specified in the standard.  What I expect is that PB will not locate the pVTbl pointers adjacent to each other like the C++ compilers will.  I took especial note of this in my Objptr demo in PowerBASIC Source code and also in the 1st of my three posts.  Do an ObjPtr() on I_X and I_Y and its my guess they are not in adjacent 4 byte slots like in the C++ code of my demo?  I don't suppose it really matters.  This is just something more or less anomalous that I noted.

     I kind of got started on it at one point in my article, but then veered into something else; C++ came before COM by a good number of years, and when Microsoft was casting about for a memory architecture that would suit their design intent of OLE, it was the pVTbl - VTbl setup that most C++ implementations were creating that caught their attention.  They settled on that as being something that would so isolate the client from the implementation details of the Server that the server would look to the client like the blackest of black boxes.  It actually looks to me like three levels of indirection - not two. 

     This weekend I'd like to clean up some minor problems here and there in the code & article, and hopefully provide a version of the dll in the download that prints out a message for every procedure in the component.  I think that would be useful for folks trying to understand the sequence in which all the calls are made.