COM in REALbasic Part Three

| | Comments (16)

Last time we made our first attempt at a COM object in REALbasic. It was a successful attempt, but it was a rather ugly implementation. Today we're going to go over some techniques for a less discongruous manner of implementing COM objects.

The key to making things as OOP as possible (I never realized OOP was an adjective, did you?) is a feature from 2007r3: namespaces. A lot of people don't see the purpose to them, but by the time you're done reading this blog post, you'll see just how clean they can make your code. We're going to continue using last entry's example and clean it up.

In case you missed it, namespaces are the feature where you are able to put class, modules and interfaces inside another module. This allows you to set scope on them in much the same way you have always been able to set the scope for module methods and properties. We're going to leverage this technique as a way to hide the COM object implementation so that the consumer doesn't have to worry themselves with implementation details.

Open up the example project, and you'll see the module named IProgressDialogSupport and the class named ProgressDialog. To begin with, let's add a new module named COMServices which is going to be the namespace for our work. After you create the new module, drag the IProgressDialogSupport module into the COMServices module. Then, drag the ProgressDialog class into the IProgressSupport module. What we've done here is started a namespace hierarchy that we can use to expose as little implementation details to the user as possible.

What we ultimately want the user to work with is an Interface representing the COM object. This is because it's the closest we can get to the way COM works natively -- you only deal with one interface at a time (which neatly solves the multiple inheritence problem that we would otherwise have). So what we're going to do is set the scope of IProgressDialogSupport to be private so that nothing outside of the COMServices module can make use of it. What's more, we're going to do the same for the ProgressDialog class as well. Finally, we're going to add a new Interface to the COMServices module named IProgressDialog whose scope is Global.

Now comes a little bit more tedium -- all of the methods that are already on the ProgressDialog class need to be moved over to the IProgressDialog interface (with the exception of the Constructor). This is because the private ProgressDialog class is going to implement the global IProgressDialog interface. The easiest way to do this is to copy and paste the methods over; the IDE will strip out the code itself. Though, this led me to an IDE bug (kdblkuft) where you still have to put the parameters in yourself. Once you have all the methods on the interface created, then you can assign the IProgressDialog interface to the ProgressDialog class. For the last little bit of cleanup, go into the IProgressDialogSupport module and mark all of the delegates as private. This is because the only thing which needs access to those delegates is the ProgressDialog class, which is part of the IProgressDialogSupport namespace.

The last bit of cleanup has to do with the COM object creation. Right now, we've shoved it off on the consumer of the COM object to have to worry about how to create the object. However, this stuff is weird -- it deals with pointers and all sorts of scary things that the consumer really shouldn't have to be concerned with. So, we're going to tie the work we just did together with a pretty bow that abstracts the user from caring about object creation.

The first step is to add a public method to COMServices called Create which takes a ByRef IProgressDialog, and returns a Boolean. Then do the same thing in IProgressDialogSupport. The COMServices method is going to simply call through to the IProgressDialogSupport method, and nothing more. The IProgressDialogSupport.Create method is where all of the interesting work happens. It is here where we will create an instance of the ProgressDialog class, which implements IProgressDialog. It will look like this:


Protected Function Create(ByRef obj as IProgressDialog) As Boolean
Declare Function CoCreateInstance Lib "Ole32" ( clsid as Ptr, punkOuter as Ptr, context as UInt32, riid as Ptr, ByRef objectInstance as Ptr ) as UInt32
Declare Sub IIDFromString Lib "Ole32" ( s as WString, p as Ptr )

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
return false
end if

obj = new ProgressDialog( p )
return true
End Function

Ta da! Now with one simple change to our original consumer code, we've successfully cleaned the COM creation up. Instead of doing all the previous creation code, we simply do:

dim c as IProgressDialog
if COMServices.Create( c ) then
    ...
end if

So to recap, what we've done is taken most of the logic and hidden it away from the consumer. We've been using some pretty advanced techniques, such as the ability to implement an interface with a private class. This makes it a lot cleaner for the user to work with COM objects, which was our ultimate goal.

As before, there's an example project which can be located here. If you have any questions, be sure to ask them. I went very quickly from "this is COM" to "BAM! Complex REALbasic code", and I did it while flying from Austin back to Riverside (so my logic may seem distracted).

16 Comments

This is very cool stuff. While I won't likely be using COM a lot (since most of my projects are xplat) these advances show something significant.

RB has progressed to the point where writing a plugin is no longer a requirement for advanced functionality as long as you know the APIs.

Honestly the only thing I've recently had to build a plugin for was to take advantage of an optimizing compiler and- if I had the time to do it by hand I could probably have come close to the speed in RB.

Yes, you can use REALbasic for some extremely powerful stuff. Delegates is just the latest in a long line of awesome power features.

How do I translate the LPCOLESTR data type to a RB data type? (for use in my own delegate tests)

BTW, this was an awesome three-parter. It answered so many of my questions about this topic. Thanks!

An LPCOLESTR is a const wchar_t * at the end of the day, so that's just a simple WString in REALbasic.

I'm glad you liked the series -- I was kind of worried that no one found it very interesting. I know it's a pretty heavy topic and all...

This was a fantastically useful and interesting tutorial. Thanks!

Now that I've had a chance to play around with COM via RB some more, I have a few more questions. I'm more at home in Mac headers, so most of my questions are about some of the Win32 header conventions and what they mean for RB.

1. Here's the proto for QueryInterface:

STDMETHOD(QueryInterface) (THIS_ REFIID riid, void **ppv) PURE;

What does PURE mean? Also, how did you decide that ppv was ByRef? Just the fact that it's **ppv (as opposed to *ppv)?

2. In DirectX headers, they use a convention I'm not familiar with…

HRESULT ( STDMETHODCALLTYPE *GetTypeInfo )(
IDXEffect * This,
/* [in] */ UINT iTInfo,
/* [in] */ LCID lcid,
/* [out] */ ITypeInfo **ppTInfo);

What is the [in] and [out] stuff about? Some of the protos, that instead of just [in] or [out], have comments like these:

[retval][out]
[iid_is][out]

Maybe this is related to the ByRef question in #1 above?

The trick to header file magic is to follow all of the strange stuff back to its original form. For instance, PURE is:

#define PURE = 0

That means the function is a pure abstract C++ function (it's an interface, not a class -- basically).

As for ppv being byref, it's mostly from experience. Whenever you see pointers (*), you have to at least consider ByRef as an option. However, the comments in the header files can help you. [in] means it's an input parameter, and [out] means it's an output parameter. When you see something defined as [out], then it's a safe bet that ByRef is the best way to go.

This come from MIDL (Microsoft Interface Definition Language) and give the programmer hints as to what everything's purpose is.

http://msdn2.microsoft.com/en-us/library/aa367088.aspx

Perfect. Thanks for the help on those questions. I do search for the definitions, but sometimes it's difficult to guess if it's a typedef or a #define or whatever… and when you get 500 search results that doesn't help much with tracking it down. Thanks again for helping me navigate these headers.

No problem (any time!) -- these can be very tricky because the header files usuually lead you down the proverbial "rabbit hole." I know I've found some items abstracted almost ten levels deep. Bleh!

OK, hopefully this will be my last question so I can stop bothering you. :-)

In your example, you use these two GUIDs:
IIDFromString( "{EBBC7C04-315E-11d2-B62F-006097DF5BD4}", riid )//IID_IProgressDialog
IIDFromString( "{F8383852-FCD3-11d1-A6B9-006097DF5BD4}", clsid )//CLSID_ProgressDialog

I'm trying to work with CaptureGraphBuilder2 (from DirectX) instead of ProgressDialog. So, I looked up CLSID_CaptureGraphBuilder2 and found something analogous to your example:

// BF87B6E1-8C27-11d0-B3F0-00AA003761C5 New Capture graph building
OUR_GUID_ENTRY(CLSID_CaptureGraphBuilder2, 0xBF87B6E1, 0x8C27, 0x11d0, 0xB3, 0xF0, 0x0, 0xAA, 0x00, 0x37, 0x61, 0xC5)

Next, I went in search of IID_ICaptureGraphBuilder2, which I could not find directly, but I found the following in a location that makes me believe it is what I should use:

MIDL_INTERFACE("93E5A4E0-2D50-11d2-ABFA-00A0C9C6E38D")

I had no idea what MIDL_INTERFACE was, but I found this:
"The MIDL_INTERFACE() and DECLSPEC_UUID() macros associate a GUID with a C++ class or struct, so that we can later get the GUID using the __uuidof() operator."

So, does this mean that I have to use __uuidof() instead of just using the GUID like your example? I'm getting an error when I try to create the object this way.

HRESULT: 0x80040154 (2147746132)
Name: REGDB_E_CLASSNOTREG
Description: Class not registered

I'm not positive that the GUID is my problem, but it's one small thing that jumped out at me while debugging. If this is a DirectX-specific issue and that isn't your cup of tea, I'll just keep experimenting.

After recreating close two twenty different classes like this, I think I need to file an RB request for some kind of easier way to do this. It took me forever to get through the tedium (e.g. redeclaring the same methods three times). Even something as simple as being able to paste text to declare a delegate would suffice, because then we could create these definitions more simply in a text editor.

No worries on asking questions -- that's how you learn! I suspect that you're very close, as you have the proper GUIDs for both the CLSID and IID. The CLSID is {BF87B6E1-8C27-11d0-B3F0-00AA003761C5} and the IID is {93E5A4E0-2D50-11d2-ABFA-00A0C9C6E38D}, just like you found. However, my guess is that you've gotten the order mixed up for the declare to CoCreateInstance. The first parameter is the CLSID of the class you want to create. The second is the IID of interface you want to retrieve from the class. Remember, a single class may implement multiple interfaces -- so, for instance, you could pass in that CLSID, but pass in the IID for an ICaptureGraphBuilder2 or an ICaptureGraphBuilder.

Make sure you're passing in the proper GUIDs for the parameters. When I tried it out, I got no errors.

You were right, I had something askew… I'm still not sure what it was, but I recoded things and then it worked. Cool!

My next problem (and then I think I'll have a pretty good handle on what's going on here):

1. I'm creating an ICaptureGraphBuilder as expected:
if COMServices.Create( win_pBuilder ) then

2. Then, I'm creating a FilterGraph again as usual:
if COMServices.Create( win_pFg ) then

3. But now I'd like to call a method of the win_pFg: SetFiltergraph, which takes a Ptr to the win_pBuilder. No matter what I try, I get an error saying that "Parameters are not compatible with this function". (ie win_pfg below)

hr = win_pBuilder.SetFiltergraph( win_pFg )

Here is the delegate and the function I am using:

Private Delegate Function SetFiltergraphFunc(this as Ptr, pfg as Ptr) As UInt32

Function SetFiltergraph(pfg as Ptr) As UInt32
dim func as new SetFiltergraphFunc( mThis.Ptr( 0 ).Ptr( 12 ) )
return func.Invoke( mThis, pfg )
End Function

And the class Interface:

Function SetFiltergraph(pfg as ptr) As UInt32
End Function

And here's the prototype from the headers:

HRESULT ( STDMETHODCALLTYPE *SetFiltergraph )(
ICaptureGraphBuilder2 * This,
/* [in] */ IGraphBuilder *pfg);

What do I need to do differently to call a function of one of these COM objects that has another COM object as a param? The headers seem to imply that a Ptr is needed, but it looks like I'm passing an IFilterGraph instead. If I can get this last piece working, I think I can figure out the rest of my functions. (fingers crossed).

Although this is painfully tedious at times, the results sound like they will be most excellent. Making progress. :-)

When you need to pass a pointer to a COM interface to an external method, it is exactly the same as when you need to pass the implicit "this" pointer to an interface's method -- you pass the mData as Ptr stored in the concrete bridge class.

The way I do it is to declare the external API (on the interface and concrete class) as taking a the COM interface in question. Then, declare the delegate as taking a Ptr. Finally, put an Operator_Convert() as Ptr onto the passed-in COM object, and have it simply return its mData ptr.

Make sense?

I thought things made sense until I tried to implement it. I think I have everything in place, but I'm getting a weird error each time I try to actually call a method off the com object. Here's what I'm doing:

//the code I'm trying to run
dim win_pBuilder As ICaptureGraphBuilder2
dim win_pFg As IFilterGraph

//this succeeds
if COMServices.Create( win_pBuilder ) then
end if

//this also succeeds
if COMServices.Create( win_pFg ) then

//but this line does not
hr = win_pBuilder.SetFiltergraph( win_pFg )

I get an unusual hr error here: E_OUTOFMEMORY (0xFFFFFFFF8007000E) - unusual because I've not encountered this error doing DirectX stuff in C/C++ with identical code.

My RB delegate looks like this:
Private Delegate Function SetFiltergraphFunc(this as Ptr, pfg as Ptr) As UInt32

My implementation in the class looks like this:
Function SetFiltergraph(pfg as IFilterGraph) As UInt32
dim func as new SetFiltergraphFunc( mThis.Ptr( 0 ).Ptr( 12 ) )
return func.Invoke( mThis, pfg )
End Function

Here's an abbreviated list of funcs in this obj from the headers, so you can see that 12 is correct:

0 - QueryInterface
4 - AddRef
8 - Release
12 - SetFiltergraph
...

And here's my class interface declaration:
Function SetFiltergraph(pfg as IFilterGraph) As UInt32
End Function

Does anything jump out at you as being obviously wrong?

I reread your earlier suggestions and reworked my code thusly:

the delegate:
Private Delegate Function SetFiltergraphFunc(this as Ptr, pfg as Ptr) As UInt32

the class method:
Function SetFiltergraph(pfg as IFilterGraph) As UInt32
dim func as new SetFiltergraphFunc( mThis.Ptr( 0 ).Ptr( 12 ) )
return func.Invoke( mThis, pfg.operator_convert )
End Function

the class interface:
Function SetFiltergraph(pfg as IFilterGraph) As UInt32
End Function

and on the FilterGraphClass I have this:
Function Operator_Convert() As Ptr
return mThis
End Function

Interestingly (or not?), I get the same error results:

hr = win_pBuilder.SetFiltergraph( win_pFg )
hr = -2147024882

If I plug this into DXErr, a util. that comes with the DX SDK, I get this:

HRESULT: 0x7ff8fff2 (2147024882)
Name: Unknown
Description: n/a
Severity code: Success
Facility Code: Unknown (8184)
Error Code: 0xfff2 (65522)

However, on my Mac calculator, I found 0xFFFFFFFF8007000E as being equal to -2147024882.

And my headers say this:
#define E_OUTOFMEMORY _HRESULT_TYPEDEF_(0x8007000EL)

So, I'm confused. I'm not sure if I have a DirectX issue here or I'm doing something wrong in RB. I have identical DX code written in C that works just fine.

There really should be a tool that generates this code for us. RB would be a lot more useful on Windows if it were easy to use COM objects.

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.