COM in REALbasic Part Two

| | Comments (5)

When we last left off, you were learning the boring pieces to what a COM object really is. Today, you're going to apply that knowledge and see how to create a COM object in REALbasic using nothing but pure RB code. We're going to accomplish that by picking a relatively easy COM object (IProgressDialog) and exposing it as a REALbasic object. However, I want to be up front with the fact that what we will accomplish today is a very crude implementation, and certainly not one you would want to mimic.

So, what is an IProgressDialog? Well, as the name implies, it's used to display a progress indication for things like file copy operations, etc. The general idea is that you create one of these, then you set up some initial information such as the title, progress animation, etc. Then, you call StartProgressDialog, SetProgress and StopProgressDialog as a way to control the UI. It's a relatively simple example of a COM object. What we are going to end up with in terms of REALbasic code is a class which does all the same work.

To start off with, we have to know the layout of the COM object. Specifically, we need to know the order of the methods in the object's vtable. Once we know that order, then we can translate that into REALbasic methods which simply call the proper method via a delegate. The basic idea is that we will have one delegate for each of the COM object's methods, and then we will have a REALbasic method which sets up and calls the delegate instance.

The order of the methods can be found by searching through header files just like you would for any sort of declare. You'll find the relevant code in ShlObj.h, and it will look like this:

DECLARE_INTERFACE_IID_(IProgressDialog, IUnknown, "EBBC7C04-315E-11d2-B62F-006097DF5BD4")
{
    // *** IUnknown methods ***
    STDMETHOD(QueryInterface) (THIS_ REFIID riid, __out void **ppv) PURE;
    STDMETHOD_(ULONG,AddRef) (THIS)  PURE;
    STDMETHOD_(ULONG,Release) (THIS) PURE;
    // *** IProgressDialog specific methods
    STDMETHOD(StartProgressDialog)(THIS_ __in_opt HWND hwndParent, __in_opt IUnknown * punkEnableModless, DWORD dwFlags, __reserved LPCVOID pvResevered) PURE;
    STDMETHOD(StopProgressDialog)(THIS) PURE;
    STDMETHOD(SetTitle)(THIS_ LPCWSTR pwzTitle) PURE;
    STDMETHOD(SetAnimation)(THIS_ HINSTANCE hInstAnimation, UINT idAnimation) PURE;
    STDMETHOD_(BOOL,HasUserCancelled) (THIS) PURE;
    STDMETHOD(SetProgress)(THIS_ DWORD dwCompleted, DWORD dwTotal) PURE;
    STDMETHOD(SetProgress64)(THIS_ ULONGLONG ullCompleted, ULONGLONG ullTotal) PURE;
    STDMETHOD(SetLine)(THIS_ DWORD dwLineNum, LPCWSTR pwzString, BOOL fCompactPath, __reserved LPCVOID pvResevered) PURE;
    STDMETHOD(SetCancelMsg)(THIS_ LPCWSTR pwzCancelMsg, __reserved LPCVOID pvResevered) PURE;
    STDMETHOD(Timer)(THIS_ DWORD dwTimerAction, __reserved LPCVOID pvResevered) PURE;
};

From this, we can see the order of the methods is QueryInterface(0), AddRef(1), Release(2), etc. So the next step is setting up delegates for each of the COM calls. This is going to feel a lot like setting up delcares because you will have to translate C code into REALbasic code with regards to parameter types and return values. The STDMETHOD macro may look rather confusing, but that essentially means that this function returns an HRESULT, which is a fancy term for UInt32. DWORD and ULONG are also UInt32 values. THIS (or THIS_) is the "self" parameter that object-oriented objects expect, and is a Ptr. LPCWSTR translates to WString, etc. I'm going to pick a few delegates to demonstrate, but they're all fairly straight-forward.

The first delegate I'll demonstrate is StartProgressDialog. We'll start off by creating a module named IProgressDialogSupport. This is where we'll put the delegates we construct. Once you have the module, add a new delegate to it, and set the name to StartProgressDialogFunc. The parameter list for the delegate will be:

this as Ptr, hwndParent as UInt32, punkEnableModeless as Ptr, dwFlags as UInt32, pvReserved as Ptr

Also, set the return type to UInt32. Once you've done that, you've declared your first COM delegate function (go have a beer!). :-)

As another example, make another delegate named SetTitleFunc. Its parameter list will be:

this as Ptr, pwzTitle as WString

And its return type will be UInt32 as well.

By this time, you probably get the picture. Go through and make a delegate for all of the remaining functions in the list. The only one which make cause problems is a REFIID, which you can just say is a Ptr for now.

Once you're done with that grunt work, we're ready to move on to actually making some use of it. To recap, what we've done so far is make declarations for all of the COM method calls available to the IProgressDialog COM object. The next step is to make a REALbasic object which actually *calls* these delegate functions. So now you need to add a new Class to the project, named ProgressDialog. Give it a constructor which takes a Ptr, and save the parameter off in a property on the class called mThis as Ptr. Don't worry about where this parameter will come from, but what it represents is the COM object pointer itself. The next step is a lot more grunt work -- remember those delegate functions you declared? Now you have to declare REALbasic methods with the same (or equivalent) parameters! Tedium at its finest, eh?

For instance, since we made a StartProgressDialogFunc delegate, we'll make a function on ProgressDialog called:

Function StartProgressDialog( hwndParent as UInt32, punkEnableModeless as Ptr, dwFlags as UInt32, pvReserved as Ptr ) as UInt32

You'll notice two things are different from the delegate: we stripped "Func" from the name, and we removed the "this as Ptr" parameter. You'll have to do this translation for the remaining functions on the object.

The final step is actually adding the guts of each of these functions, which will be yet another tedious process. What we need to do is create an instance of the delegate for each function, and call through to it. The thing to remember is that the mThis pointer we've stored points to a vTable. This vTable is an array of Ptrs with one for each function the COM object supports. This is where that order information comes in handy. Continuing with our example method:


Function StartProgressDialog( hwndParent as UInt32, punkEnableModeless as Ptr, dwFlags as UInt32, pvReserved as Ptr ) as UInt32
  dim func as new StartProgressDialogFunc( mThis.Ptr( 0 ).Ptr( 12 ) )
  return func.Invoke( mThis, hwndParent, punkEnableModeless, dwFlags, pvReserved )
End Function

(In case you are wondering where the magic number 12 came from, it is the offset into the vtable where the StartProgressDialog function pointer resides. It is the 3rd index in (zero-based), and 3 * 4 = 12.)

Now you get to go through the function list one more time and put similar code into every method. But, once you've done so, you've got a fully-functional REALbasic class which represents a COM object. And it was done purely in REALbasic code! The only step remaining is constructing the COM object, and by extension, a functional instance of the REALbasic object. However, after all that grunt work, this is an easy step.

The way you make most COM objects is with a call to CoCreateInstance. This function takes a class GUID for the creatable class type, and an interface GUID for what COM interface you are interested in. This may seem slightly strange at first blush, but the thing to remember is that a COM object may implement multiple COM interfaces, however, you only work with one interface at a time. So you can think of it as a weird typecasting, of sorts. To create our progress dialog object, we need to make a new ProgressDialog COM object (CLSID_ProgressDialog), and we want an IProgressDialog COM interface for it (IID_IProgressDialog). Both of these GUIDs can be found by searching header files. To save you the hassle of searching, the IID_IProgressDialog is "EBBC7C04-315E-11d2-B62F-006097DF5BD4", and the CLSID_ProgressDialog is "F8383852-FCD3-11d1-A6B9-006097DF5BD4" (found in ShlGuid.h).

Let's take a look at the declare for CoCreateInstance next, since that's what we will be using to create an instance of the COM object.

Declare Function CoCreateInstance Lib "Ole32" ( clsid as Ptr, punkOuter as Ptr, context as UInt32, riid as Ptr, ByRef objectInstance as Ptr ) as UInt32

Phew, that's a lot of Ptrs! The first pointer is a pointer to our CLSID object, which we have to create from the string. The second object can safely be ignored for this discussion, and is nil. The third pointer is the IID object (also created from the string). The final pointer is the COM object we wanted to create (it's an output parameter). The trick to converting our string versions of the GUID into a Ptr version is the function IIDFromString.

Declare Sub IIDFromString Lib "Ole32" ( s as WString, p as Ptr )

Putting it all together will yield code that looks like this:


dim clsid as new MemoryBlock( 16 )
dim riid as new MemoryBlock( 16 )

IIDFromString( "{EBBC7C04-315E-11d2-B62F-006097DF5BD4}", riid )
IIDFromString( "{F8383852-FCD3-11d1-A6B9-006097DF5BD4}", clsid )

Const CLSCTX_INPROC_SERVER = &h1
dim p as Ptr
if CoCreateInstance( clsid, nil, CLSCTX_INPROC_SERVER, riid, p ) <> 0 then
MsgBox "Failed to create the Progress Dialog"
return
end if

dim c as new ProgressDialog( p )

If you run the code and step through it, you should see that you have a valid ProgressDialog object, which means you've finally created your first COM object in pure REALbasic code! Now it just boils down to using it:


Const PROGDLG_AUTOTIME = &h2
Const PDTIMER_RESET = &h1
call c.SetTitle( "Slow Process!" )
call c.StartProgressDialog( self.Handle, nil, PROGDLG_AUTOTIME, nil )
call c.Timer( PDTIMER_RESET, nil )
for i as Integer = 0 to 10
  if c.HasUserCancelled then  exit for
  call c.SetLine( 2, "I'm processing " + Str( i ), false, nil )
  App.SleepCurrentThread( 2000 )
  call c.SetProgress( i, 10 )
next i
call c.StopProgressDialog
call c.Release

Now, as I said before, this isn't the cleanest format we could possibly write. However, it does demonstrate that it is now possible (though tedious) to consume COM objects from within REALbasic.

To download the example project which has done all the work for you, you can grab a copy here.

Tune in next time for some tips and tricks on how to make a better implementation of COM objects that's a bit cleaner.

5 Comments

That's great stuff, Aaron, but the code is being completely cut off when it's too long. Any way to fix that? This is a very interesting subject to me...

Alas, but no. I know so very little about webby stuff, and MT specifically, that I'd end up totally hosing everything to even try!

That's why I've made a project available for download. That way you can follow along somewhat.

Sorry!

Note: The code isn't actually cut off, it just appears that way on screen. You can copy the whole article and paste it into a word processor or an email and the full code will be visible.

Hi Aaron,
Great intro to COM in RB, but I have a question.

you mention: ShlObj.h Most of the COM objects that I work with are in the form of dll's. IN VB people use dim withevents to interact with them.

Would the above method allow me to work with these objects? How would one find the method order with out a header file?

@James -- REALbasic's support for COM is rudimentary at best, so there's nothing nearly as automatic as VB's WithEvents, unfortunately. As for finding out the method order without a header file -- you can either use MSDN (it usually shows methods in vtable order, or is at least explicit about whether it's alphabetical or vtable order), or you can contact the DLL provider for the information. If you have access to a type library reader, that will also show you the vtable order (usually).

Leave a comment

Disclaimer

I'm currently an employee of REAL Software. My blog is mine. The opinions represented in this blog are mine as well and may not represent my employer's opinions. All original material is copyrighted and property of the author.

REALbasic® is a registered trademark of REAL Software, Inc. REAL SQL Server™ and Lingua™ are pending trademarks of REAL Software, Inc. All rights reserved.