Developer

Learn about VB's class method sleight of hand

This edition of VB Q&A addresses the question of why AddressOf can't be used with class methods. The answer lies in VB's implementation of COM, which makes your classes not quite what they appear.


Q: I want to use the AddressOf operator to send the address of a VB class function to an API function, but I see in the documentation that AddressOf can be used only with functions and subs declared in BAS modules, never in class modules. Why is this? It's terribly inconvenient. Is there a way around this limitation?

A: The short answer to this question is that AddressOf can't be used from a class module because of the way VB hides some underlying parts of COM from you. To handle a callback in a class module, you'll have to resort to using a third-party tool, some helper code written in a language that understands function pointers (Delphi or C++) to wait for the callback and possibly raise an event back into your class when it occurs, or a rather sloppy workaround—which I'll explain later.

Deep in the heart of COM
Not satisfied with that explanation? I didn't think you'd be. To get a better look at why this is a problem, we'll have to dig a little deeper into the COM specification. There, we'll find two rules that all COM objects (including your VB6 class) have to follow. VB cleverly hides these rules from you but drastically alters your method declarations in the process. Functions and Subs declared in BAS modules aren't subjected to this sleight of hand, so they aren't affected and can be used with AddressOf without any problems.

COM methods return an HRESULT
It's considered good form for all COM methods to return status information to their caller. This information is codified into a formalized type of result code known as an HRESULT. If you've ever poked around any of the C headers that come with Visual C++, you might have seen variables of this type all over the place. These result codes can convey error codes, indicate success, or relay any other desired information to the COM object's client.

Why are COM errors always negative?
HRESULT codes are formalized to the extent that error codes are always supposed to be negative, zero means success, and nonerror status messages are supposed to be positive. When you call a COM method, VB checks the HRESULT return code for you. If it's negative, VB populates an error object with appropriate context information, including the return code, and raises it in your application. That's why COM error codes always seem to be negative. Incidentally, VB discards any nonnegative HRESULT codes, so even if a component returns nonerror status information in an HRESULT, you won't see it.

Now, since a VB class is also a COM component, it too returns status information via an HRESULT. The thing is, your VB code never sees it. VB, which really thinks it's doing you a favor, intercepts the HRESULT and does some internal stuff with it. To do this, VB essentially has to change the declaration of any functions in your class modules to match the COM convention. Let's assume you've written the following function for the class ExampleClass:
Public Function DoSomething(strMessage as String) as Boolean

When you compile your class module, VB essentially rewrites ExampleClass.DoSomething to look something like this instead:
Public Function DoSomething(strMessage as String, _
Result as Boolean) as HRESULT

Get it? VB rewrites all class functions so that they receive an implicit extra parameter of the same type as the function's return type. Subs, on the other hand, are altered to become functions with a return type of HRESULT.

Implicit this pointers
As if that's not bad enough, COM also specifies that all method declarations have to include a pointer to an instance of the method's object as the first parameter in the parameter list. This is a holdover that allows non-OOP languages like C to be able to work with COM components. Some COM-aware languages will require the pointer in any COM interface function, while others (like VB) automatically add and remove the reference for you, reasoning that if it always has to be there, you don't need to be troubled with remembering it.

Since you won't be seeing it in your code (and couldn't do anything with it even if you did), VB must further alter your method headers to include the required pointer. To illustrate using the ExampleObject.DoSomething example I introduced above, the actual declaration would look something like this, with MyAddress being a pointer to the address of an instance of ExampleObject:
Public Function DoSomething(MyAddress as Long, _
strMessage as String, Result as Boolean) as HRESULT

Since you can't create a class in VB without also creating a COM component, this behavior is really unavoidable. Unless your callback function is supposed to include a pointer to an arbitrary memory location and an oddball return code in its declaration, it simply won't match the expected declaration, and any callback will fail. So the VB programming team elected to simply disallow the use of AddressOf on a class member.

A kludge—I mean less-than-elegant—workaround
Being able to declare callback functions only in a BAS module and not inside a class module is a pain. You obviously need a class instance in order for any method calls to be valid, and your BAS module callback function will lack that instance reference.

A somewhat sloppy solution is to include some way for the BAS module to hold a reference to your class. That way, when your callback function is called, it has the context required to call any needed class methods. Your BAS module function is then essentially a surrogate for the class method that needs to be notified of a callback. A single variable would likely be insufficient here, unless it's not possible for the external function to be called twice before the first callback is made. You could wind up with the wrong instance of the class being notified of a callback. Look into using a collection instead.

Editor's Picks

Free Newsletters, In your Inbox