COM RAQs
These are the answers to some Rarely Asked Questions that ought to be asked more often.
How can I make a copy of an object?
You can’t.
As you should know by now, setting an object variable to another object variable makes another reference to the same object. It doesn’t create a new object. But there might be times when you really do want multiple copies of an object. Surely there must be some language feature that allows you to make a copy if you really want one. Or failing that, there must be a way to hack it with CopyMemory.
Nope.
This problem isn’t unique to Visual Basic. Every object-oriented language has it. The trouble is, the only thing that knows how to copy an object is the object itself. The language can’t do it and the operating system can’t do it. To understand why, let’s look at what it would take to copy some classes.
The CSieve class described earlier in this chapter has four internal variables that keep track of the state of the sieve algorithm. If you could somehow copy those four internal variables from a sieve object to a sieveNew object of the same type, you’d have an exact duplicate. Even if you made the copy just after you discovered that 1427 is the prime number after 1423, you could keep calculating with sieveNew and it would tell you the same thing as sieve—that 1429 is the next prime number in line.
So why doesn’t Visual Basic provide a CopyObject procedure that would do just that? Because it’s more easily said than done. One of the private members of CSieve is a dynamic array whose size is determined at run time by the value of one of the other members. The language can’t determine how you arrived at the size of the array, but in this case it could theoretically check the UBound and LBound to determine the size of the array. Some classes could be duplicated with a simple CopyMemory, but others would involve more complex
calculations. It is possible to imagine a CopyObject procedure that could successfully duplicate CSieve objects.
But what about the CPictureGlass class described in Chapter 7? You might very well want to copy a transparent bitmap object and move the copy to another area of the screen. The CPictureGlass class has several private hdc variables. If you copied those variables to another object, you wouldn’t really have a copy. The hdc variables of both objects would be referring to the same device contexts. If you changed the position of one copy, the other would move with it. To successfully copy the object, you’d need to create new device contexts for the copy, but give them the same state as the old ones. I’m not even going to think about what kind of Windows API calls that might require, but I can tell you there’s no way the language could provide a CopyObject procedure to do it for you.
Copywise, every class is different. Classes that refer to files or registry entries or other complex state data must define for themselves what it means to copy that data. The only thing a language can do to help is to provide some standard mechanism for defining your own copy mechanism.
C++, for example, provides copy constructors and redefinition of the equality operator as standard means of defining copy semantics. While Visual Basic might someday get constructors, let’s hope it never gets operator overloading. Java provides a more likely scenario. It provides (through a mechanism that need not concern us) a standard Clone method. Any class that wants to be cloneable can implement a Clone method that defines what copying means. A client of the class can call the standard Clone method to create a copy.
Visual Basic doesn’t provide any standard mechanism for copying, but you could easily define your own standard. For example, for every class you think should be copyable, provide a method with the following signature:
Function Clone() As <Type>
Here’s some air code showing how the Clone method might be implemented for the CSieve class:
Function Clone() As CSieve
Dim sieveNew As New CSieve
With sieveNew
.MaxPrime = iMaxPrime
.Primes = cPrime
.Current = iCur
.CopyArray af
End With
Set Clone = sieveNew
End Function
You can’t actually write this method for the CSieve because MaxPrime is the only member used that currently exists. CSieve has a Primes property, but it’s read only. To implement the Clone method, you need to add a Primes Property Let procedure, a Current Property Let Procedure, a Current Property Get procedure, and a CopyArray method. Since no one other than you will need these members, you can make them Friends. Without these Friend members, you have no means to create a duplicate object.
The Clone standard could even work in situations where you don’t know whether the objects can be cloned. For example, you could use code like this:
On Error Resume Next
For Each obj In oldobjects
‘ Copies cloneable objects, ignores the rest
newobjects.Add obj.Clone
Next
In real life this isn’t as much of a problem as you might expect. Most objects don’t need to be copied, and when they do, you can provide a method that copies them.
What’s the difference between ByVal and ByRef object parameters?
You can use ByVal and ByRef on object parameters just as you can with intrinsic types. They look the same. You use them for the same reasons. But they work very differently. And their performance is different, depending on the type of COM marshaling being done.
Let’s review how ByVal and ByRef work for intrinsic types. Assume the following test procedure:
Sub TestIntParam(ByRef iRefp As Integer, ByVal iValp As Integer)
iRefp = 5: iValp = 5
End Sub
You call it with the following code:
Dim iRef As Integer, iVal As Integer
iRef = 4: iVal = 4
TestIntParam iRef, iVal
Debug.Print “ByRef integer: “ & iRef
Debug.Print “ByVal integer: “ & iVal
The results are:
ByRef integer: 5
ByVal integer: 4
Changes made internally to the ByVal parameter have no effect on the outside integer because it’s a different copy.
The same thing happens with strings, although, internally, they work quite differently. If you pass a string by value, Visual Basic has to allocate a new string and copy each character. When you write this:
Sub DoString(ByVal s As String)
§
Visual Basic does this:
Sub RealDoString(ByRef sParam As String)
Dim s As String
s = sParam
§
This extra work is more expensive (see the “Performance” sidebar on page 551) so normally, Visual Basic programmers pass strings by reference and take extra care not to modify arguments intended for input only. But if you really need a temporary string and you don’t want changes to it to affect the original, you might as well let Visual Basic create that string through a ByVal parameter rather than declaring and creating it yourself.
This leads back to our original topic—passing objects by value. In the following example, assume that the CParam class has a string property called Name.
Sub TestObjParam(ByRef refP As CParam, ByVal valP As CParam, _
ByRef refNewP As CParam, ByVal valNewP As CParam)
Dim refNew As New CParam, valNew As New CParam
‘ Change properties
refNew.Name = “Changed”: valNew.Name = “Changed”
‘ Change objects
Set refNewP = refNew: Set valNewP = valNew
refP.Name = “Changed”: valP.Name = “Changed”
End Sub
You call the test procedure like this:
Dim ref As New CParam, val As New CParam
Dim refNew As New CParam, valNew As New CParam
ref.Name = “Unchanged”: val.Name = “Unchanged”
refNew.Name = “Unchanged”: valNew.Name = “Unchanged”
TestObjParam ref, val, refNew, valNew
Debug.Print “ByRef object: “ & ref.Name
Debug.Print “ByVal object: “ & val.Name
Debug.Print “ByRef new object: “ & refNew.Name
Debug.Print “ByVal new object: “ & valNew.Name
The results are:
ByRef object: Changed
ByVal object: Changed
ByRef new object: Changed
ByVal new object: Unchanged
At first you might think that passing an object variable by value would cause the compiler to make a copy of the object just like it makes a copy of a string. Changing properties of the ByVal object wouldn’t affect the outside object. However, as I pointed out in the last section, Visual Basic doesn’t know how to make a copy of the object and couldn’t do so even if it wanted to. But it doesn’t want to because you didn’t pass an object by value—you passed an object variable by value. Visual Basic copies the object variable, not the object. The copied object variable still references the same object, and any changes to the object still can be seen from the outside.
NOTE All bets are off when you pass objects of type Object. Late-bound objects work by different rules because it’s not a simple matter for Visual Basic to decide what you really want when you change the object variable. I’m not going to get into the details because I’ve been trying to persuade you not to pass late-bound objects around anyway. If you really need to do this, just be aware that you’ll need to do further research to figure out the different rules.
The second part of the test demonstrates that changing the object variable inside the procedure affects the outside variable passed by reference, but has no effect on the outside variable passed by value. That might not be the obvious behavior, but you can see that it’s right if you think it through.
Now for the performance issues. When you pass an object variable by value, a new copy of that variable is created and set to the same object. You write this:
Sub DoObject(ByVal obj As CParam)
§
Visual Basic does this behind the scenes:
Sub RealDoObject(ByRef objParam As CParam)
Dim obj As New CParam
Set obj = objParam
§
The hidden Set statement causes an AddRef to increment the reference count. A Release is done when the object variable goes out of scope. There’s a cost for the hidden AddRef and Release. As long as you’re dealing with local or in-process classes, it’s more efficient to pass objects by reference. If you’re changing only the object, not the object variable, you might as well pass by reference. This is the normal case.
Things are a little different when you pass objects between processes, threads, or machines. In this case, the cost of AddRef and Release is dwarfed by the cost of marshaling the data across the boundary. If you pass a parameter ByVal, you’re saying that the parameter has to be marshaled across the boundary, but it doesn’t need to come back. If you pass by reference, you’re saying that the transfer has to go both ways. If the procedure won’t modify the object variable, it’s much more efficient to pass it by value.
Can I get the current reference count?
No.
The reference count is none of your business. You’re not supposed to know its value. The only thing that matters is whether it’s zero, and that only matters to Visual Basic, not to you. Every class decides how it wants to store and maintain the reference count. A COM object can be written in any number of languages, and different languages can arrange private class data differently. Even if the class made the obvious decision to store the reference count in a member variable, you couldn’t know for sure where the variable was in relation to the start of
the class.
Yes, but can I get the reference count?
What do you want with the damn reference count anyway?
Well, I’m debugging a Visual Basic object that is supposed to go away, but it doesn’t. If I could check the reference count at various points in the program, maybe I could tell what’s going wrong.
OK, but don’t call me if you have trouble:
Public Function VB5GetRefCount(ByVal pUnk As IUnknown) As Long
If pUnk Is Nothing Then Exit Function
‘ Get count from magic offset in object
CopyMemory VB5GetRefCount, ByVal ObjPtr(pUnk) + 4, 4
‘ Adjust to account for references to parameter
VB5GetRefCount = VB5GetRefCount - 3
End Function
It’s not in VBCore or anywhere else on the CD. You have to type it in yourself. If it fails, it might be because your hair color is different from mine. I’m not even going to try to explain why it works. It won’t work for out-of-process objects. It probably won’t work for objects written in languages other than Visual Basic. And it might not work for Visual Basic objects in future versions. All I can say is that it worked for me on at least one occasion for at least one class written in Visual Basic version 5.
Why can’t I pass structures as public method parameters or properties?
Shut up and eat your mush.
Wait a minute! C++ programmers can pass structures to COM objects. Why can’t Visual Basic programmers do the same?
Because C++ programmers write their own type libraries. The complete type library syntax lets you marshall many kinds of language-specific data. Visual Basic creates type libraries for you automatically behind the scenes, but it knows only how to use the standard COM Automation types, which just happen to be exactly the same as the Visual Basic types.
While it’s true that C++ programmers can pass structures, arrays of various sorts, unsigned integers, and simple null-terminated strings (LPSTR type) through COM servers, it’s also true that such servers can be recognized only by clients developed in C++ or other low-level languages. Making a component language-
specific defeats one of the main purposes of COM. There might be a few cases where a C++ to C++ connection is exactly what is needed, but generally controls and other general-purpose components should follow the COM Automation standards so that they can be used by any client.
But why doesn’t COM Automation recognize structures?
Well, it could, and I’ve heard rumors that it will in some future version. But what will it allow you to put in those structures? Different languages handle data types differently—especially strings and arrays. The Windows API already uses some C-specific data structures that are very difficult to use from Visual Basic. We don’t need more of that confusion. Even if COM Automation allows structures in the future, those structures will probably be limited to a subset of data types that any COM Automation client can recognize.
OK, but that doesn’t help me with my code. I need to pass some structured data in parameters and properties. How am I going to do it?
Well, there are several workarounds. First, if your objects are local, you can pass UDTs around anywhere you like as long as you make your methods and properties Friends. The Friend keyword bypasses the normal COM marshalling system. A Friend parameter or property can have any type—just like a private procedure or a procedure in a standard module.
That’s handy to know, and I’ll keep it in mind for future projects. But right now I need to pass structured data to an ActiveX component. How am I going to do it?
There’s a hack, and there’s a solution, but you probably won’t like either one. The hack is to pass your data in byte arrays. Let’s say your client has a UDT with some structured data that you want to pass around. You ReDim an array of bytes with the same size as the structure, then CopyMemory the UDT to the array and pass the array to the server as a property or method parameter. The server knows that the array actually represents a UDT, so it declares a UDT variable of the right type and CopyMemorys the array to the UDT. Alternatively, both sides can treat the data as a blob, using the byte access techniques described in “Reading and Writing Blobs” on page 277, to get or put data at the appropriate offsets. This hack isn’t much fun, but there are times when it’s worth the trouble.
Usually it’s better to change your mind rather than try to fit your code into old mindsets. If you’ve got a design that uses lots of UDTs, chances are you’re trying to fit an old structured programming design into an object-oriented mold without much work. Think again, then do the right thing. Redesign your structures as classes. Turn the fields of your UDTs into properties. Convert procedures that take UDT parameters into methods. In most cases, you’ll end up with a better design anyway, and you might even save coding time in the long run.