15 July 2011

COM (OOP) in GB32 - IUnknown (1)

In this part I'll show you the layout of a COM class as it could be implemented in GFA-BASIC 32. Note that we restrict ourselves to a minimalist COM object that supports the IUnknown interface only. We will not yet create properties and methods.
The GFA-BASIC 32 approach is loosely based on the article series COM in plain C by Jeff Glatt, specifically Part 1 and Part 2. However, in the first step we are not going to use type libraries, we are not going to store the COM object in a DLL, and we are not going to define another COM object that creates the one we define. COM is merely a binary standard; it dictates how to layout an array of function pointers and where to put the address of this array (of function pointers). It also dictates how to add reference counting. To comply to this standard a COM object must at least contain three pointers to pre-defined functions, also known as the IUnknown interface. In GFA-BASIC 32 this could look like this:
// IUnknownBlog.g32 17.07.2011
Debug.Show

// Our COM object: an implementation of IUnknown
GUID IID_IUnknownImpl = 9578fdab-97cb-4322-99e4-699abd26be1d
Type IUnknownImpl
  lpVtbl As Pointer IUnknownImplVtbl
  RefCount As Int
EndType

// VTABLE (an array of function pointers)
Type IUnknownImplVtbl
  QueryInterface As Long
  AddRef As Long
  Release As Long
EndType
Static IUnknownImplVtbl As IUnknownImplVtbl
With IUnknownImplVtbl
  .QueryInterface = ProcAddr(IUnknownImplVtbl_QueryInterface)
  .AddRef = ProcAddr(IUnknownImplVtbl_AddRef)
  .Release = ProcAddr(IUnknownImplVtbl_Release)
EndWith

// First create a heap allocated instance
// of our implementation of IUnknownImpl
Dim pIUnk As Pointer IUnknownImpl
Pointer pIUnk        = mAlloc(SizeOf(IUnknownImpl))
Pointer pIUnk.lpVtbl = *IUnknownImplVtbl
pIUnk.RefCount = 1

Dim obIUnk As Object
{V:obIUnk} = V:pIUnk
Trace obIUnk


Dim o As Object // assign to other
Set o = obIUnk  // AddRef() call
// two calls to Release
/*** END ***/

GUID IID_IUnknown      = 00000000-0000-0000-c000-000000000046
Global Const E_NOTIMPL = 0x80004001
Global Const E_NOINTERFACE = 0x80004002
Declare Function IsEqualGUID Lib "ole32" (ByVal prguid1 As Long, ByVal prguid2 As Long) As Bool

Function IUnknownImplVtbl_QueryInterface( _
  ByRef this As IUnknownImpl, riid%, ppv%) As Long Naked
  Trace Hex(*this)
  {ppv} = Null
  If IsEqualGUID(riid, IID_IUnknownImpl) || _
    IsEqualGUID(riid, IID_IUnknown)
    {ppv} = *this
    IUnknownImplVtbl_AddRef(this)
    Return S_OK
  EndIf
  Return E_NOINTERFACE
EndFunc

Function IUnknownImplVtbl_AddRef(ByRef this As IUnknownImpl) As Long Naked
  Trace Hex(*this)
  this.RefCount++
  Return this.RefCount
EndFunc

Function IUnknownImplVtbl_Release(ByRef this As IUnknownImpl) As Long Naked
  Trace Hex(*this)
  this.RefCount--
  If this.RefCount == 0 Then ~mFree(*this)
  Return this.RefCount
EndFunc
Copy it to a new GFA-BASIC 32 application and save as IUnknownBlog.g32. Try to run it; it should compile and run flawlessly. Our first minimalist COM object/class has been defined and is up and running. Of course it doesn't do anything, but the we have an object that integrates with GFA-BASIC 32 and fully complies to the COM binary standard.
Assign to Object
Let us take a brief look at the code. Since this project isn't meant for the beginner, I'll walk you through it in big steps.
The Object data type holds a pointer to a piece of memory of at least 4 (lpVtbl) bytes. Mostly it contains another integer for a reference count. In GFA-BASIC 32 the Ocx variables always define their count in the second slot, we will use that as well.
To put a memory address in an Object variable we usually use Set obj2 = obj2. GFA-BASIC 32 checks for two proper COM object types at compile-time. Since we have mAlloc-ed address only this syntax wouldn't work. We must write it to Object directly. For the same reason we set the RefCount to 1 by hand.
The array of functions (VTABLE) is stored in a Type and shared with all instances of our custom COM object. Therefor a static (global) variable is used and initialized once. Each new COM object should hold the vtable-address in its lpVtbl member.
_QueryInterface, _AddRef, and _Release
The application COM functions _QueryInterface, _AddRef, and _Release are never called directly, but always by GFA or another COM object. They clutter up our application code and are in fact just boiler plate code. We must do something about that, for instance put them in a $Library. Also note the Naked attribute. These functions are never executed in the context of the application and need no TRACE code and Try/Catch-exception handler. They should be as naked as possible to gain the best performance.
The Trace *this commands in the code are to verify the address passed. Also note the type of the this pointer. COM passes the address of the COM object by reference and by adding the correct type into the function declaration we can access its members directly. All other parameters are simple placeholders for addresses we don't use or pass on.
In the next part we will implement an IDispatch object and try to integrate the COM code more into GFA-BASIC 32.