Anatomy of a Basic Procedure Call


To pass by value or not by value: that is the question. To answer it, you need to go farther down than Basic programmers normally go. Programming languages from Basic to C use the stack to pass arguments from caller code to a callee procedure. I’ll first examine how this works for calls to Basic procedures and later expand our knowledge to cover calls to API procedures.


Basic programmers can afford to have a simplified view of the stack as an area of memory used for temporary one-way communication. The caller puts arguments on the stack and then calls a procedure. The callee can examine or modify the information. As soon as the callee returns, the portion of the stack used by the callee disappears. (Actually, it is reused.) In other words, the stack is write-only to the caller and effectively read-only to the callee (because no one will be around to see the results of stack writes). The purpose of this mechanism is protection: caller and callee can’t access each other’s data without permission.


There are as many ways to pass data on a stack as there are languages and data types. When a caller in one language (say, Basic) tries to pass data to another (say, C), the connection works only if both languages agree on a convention. The caller must put the data on the stack in the exact place and format expected by the receiver. For now, we’re concerned with only one aspect of calling conventions: whether the caller and the receiver agree to pass by value or by reference. Left to their own devices, Basic will pass by reference, and C will receive by value.


As an example, take the ZapemByRef and ZapemByVal procedures defined in the ZAPI library. This new dynamic-link library (ZAPI.DLL) makes it easy for Windows-based programs to zap space aliens in a consistent and portable manner, regardless of what Zap hardware and Zap device driver happen to be attached to the computer. If the ZapemByRef procedure were written in Basic, it might look something like this:

Sub ZapemByRef(ordAlien As Integer)
If ordAlien = ordMartian Then
‘ Do whatever it takes; then set 0 for successful zap
ordAlien = 0
End If
‘ Handle other aliens
End Sub

Now assume that this procedure is called with the following lines:

Const ordMartian = 7
§
Dim ordCur As Integer
ordCur = ordMartian
ZapemByRef ordCur
If ordCur = 0 Then BuryEm

Figure 2-3 shows what the stack looks like to the caller and to the callee. The caller passes its argument using the default Basic convention of calling by reference. It puts the address of the variable being passed on the stack.



Figure 2-3. Passing by reference.


Giving another procedure an address is an act of trust. You’ve given away the key to modifying whatever is located at that address (and, incidentally, any adjacent addresses). In this case, the ZapemByRef procedure can read or write the parameter (which it calls ordAlien, even though the address is actually the same as ordCur). Technically, reads and writes are done indirectly through a pointer, an operation that takes a little more processing than modifying a variable directly. Basic hides this, however, and makes writing to a by-reference parameter look the same as writing to any other variable.

ZapemByRef is a textbook example of bad design. To mention only one of its problems: what if a user passed the ordMartian constant directly instead of assigning it to a variable? Would the caller really pass the address of a constant? How constant would that constant be if you were passing its address around? What if the user passed the number 7 instead of ordMartian? It turns out that passing a constant by reference is perfectly legal, but Basic implements this feature by creating a temporary variable and passing the address of that variable. ZapemByRef could then write to that variable (using the ordAlien alias), but the caller wouldn’t be able to check the results because it wouldn’t have a name for the temporary variable.

Problem: How does the timing of arguments passed by value compare to the timing of arguments passed by reference (the default)?


Problem Native Code P-Code
Integer by value 0.0063 sec 0.2508 sec
Integer by reference 0.0069 sec 0.2518 sec
Long by value 0.0073 sec 0.2563 sec
Long by reference 0.0072 sec 0.2553 sec
Single by value 0.0072 sec 0.2521 sec
Single by reference 0.0073 sec 0.2585 sec
Double by value 0.0078 sec 0.2579 sec
Double by reference 0.0073 sec 0.2599 sec
Variant by value 0.1585 sec 0.5561 sec
Variant by reference 0.0602 sec 0.4222 sec
String by value 0.3388 sec 0.8279 sec
String by reference 0.1669 sec 0.5901 sec


Conclusion: For intrinsic numeric types, there isn’t enough difference to spit at. Notice what happens as the variables get larger. A reference variable is always­ four bytes, but if you have to push all eight bytes of a Double or all 16 bytes of a Variant onto the stack, it’s going to cost you. I threw strings into the table for comparison, but in fact they work a little differently. You’re not really saving the whole string on the stack when you pass a string by value. You are making an extra copy though—through a mechanism that we won’t worry about in this chapter. Just take a look at the extra cost, and then make the obvious decision: always pass strings by reference in your Basic code. Calling API functions is a ­different matter.


Let’s move on to ZapemByVal. This procedure is a function that returns a Boolean value to indicate success or failure:

Function ZapemByVal(ByVal ordAlien As Integer) As Boolean
If ordAlien = ordMartian Then
‘ Do whatever it takes; then set True for successful zap
ZapemByVal = True
Exit Function
End If
‘ Handle other aliens
End Function

Now assume that this function is called with the following lines:

Const ordMartian = 7
§
If ZapemByVal(ordMartian) Then DoWhatNeedsToBeDone

This looks better. The call takes fewer lines of code because the constant is passed directly. Success or failure comes back through the return value.


Under the hood, caller and callee treat the argument in completely different ways. Instead of copying the address of the argument onto the stack, the caller copies the value. Figure 2-4 shows the stack from the viewpoint of both caller and callee. If ZapemByVal were to modify the ordAlien variable, the stack value would change, but this value will disappear into the sunset as soon as Zap­emByVal returns. So if the function happens to need a scratch variable, there’s no technical reason not to use ordAlien for this purpose after it has been read. (In practice, however, using a variable for anything other than what its name implies is a good way to write unmaintainable code.)


Which parameter passing method should you choose? Since passing by reference is the default, the temptation is to accept it without thinking for Basic



Figure 2-4. Passing by value.


procedures. And if you compare the timing of the two methods, as I did in the “Performance” sidebar on page 54, you’ll see that this isn’t a bad strategy. Results vary depending on the arguments, but the difference isn’t great enough to justify changing code. Other factors, many of which I will discuss later, are more important. When you are dealing with Windows API calls, however, the primary factor is what Windows tells you to do. Why Windows chooses one method or another in different circumstances should tell you something about choices in your Basic code.