Recipe 7.2 Create a Global Procedure Stack
7.2.1 Problem
When you're writing an
application, you often need to know the name of the current procedure
from within your code. For example, if an error occurs,
you'd like to be able to have a generic function
handle the error and display the name of the procedure in which the
error occurred (and all the procedures that have been called on the
way to get there). VBA doesn't include a way to
retrieve this information. How can you accomplish this?
7.2.2 Solution
By maintaining a list of active procedures, adding the current name
to the list on the way into the procedure and removing it on the way
out, you can always keep track of the current procedure and the
procedure calls that got you there. There are many other uses for
this functionality (see the next solution, for example), but one
simple use is to retrieve the name of the current procedure in a
global error-handling procedure.The
kind of data structure you'll need for maintaining
your list is called a stack . As you enter a new
procedure, you "push" its name onto
the top of the stack. When you leave the procedure, you
"pop" the name off the stack. Figure 7-2 shows a graphical representation of a
procedure stack in action. The arrows indicate the direction in which
the stack grows and shrinks as you add and remove items.
Figure 7-2. The call stack and the sample routines to fill it
To see the procedure stack in action,
load 07-02.MDB . Open the module basTestStack in
design mode. Open the Immediate window (choose View
Immediate Window). In the Immediate window, type:
? A( )
to execute the function named A. Figure 7-2 shows A
and the procedures it calls. At each step, the current procedure
pushes its name onto the procedure stack and then calls some other
procedure. Once the calling procedure regains control, it pops its
name off of the stack. In addition, each procedure prints the name of
the current procedure (using the acbCurrentProc
function, discussed later in this solution) to the Immediate window.
Once all execution has finished, you should see in the Immediate
window output like that shown in Figure 7-3.
Figure 7-3. The output from running the sample procedure
Follow these steps to incorporate this functionality into your own
applications:
- Import the module basStack into your application. This includes the
procedures that initialize and maintain the procedure stack. - Insert a call to the
acbInitStack subroutine into code
that's executed when your application starts up.
Consider adding this procedure call to the code in your main
form's Load event procedure. You'll
want to call acbInitStack any time you restart
your program during development, so you probably
don't want to call it from the Autoexec macro, which
is executed only when you first load the database. To call
acbInitStack , either place its name alone on a
line of code, like this:acbInitStack
or use the Call construct, as follows:Call acbInitStack
- For each procedure in your application, place a call to
acbPushStack as the first statement. This
procedure will place the value it's passed on the
top of the stack. As the single argument for each call, pass the name
of the current procedure. Our example places a pair of parentheses
after function names and nothing after subroutine names, as a matter
of style. As the last line in each procedure add a call to
acbPopStack , which will remove the current name
from the top of the stack. - You can retrieve the name of the currently executing procedure at any
time by calling the acbCurrentProc function.
This function looks at the top of the stack and returns the string it
finds there. You can use this as part of an error handler or, as in
the next solution, to help track procedure performance.
7.2.3 Discussion
The module you imported from 07-02.MDB ,
basStack, includes code for maintaining the procedure stack and a
module-local variable that is the stack itself. There are just six
entry points (nonprivate procedures) in the module. Table 7-1 lists those procedures. Since all the code for
the stack is encapsulated in that one module, you never really have
to know how it all works. However, it's quite
simple.
Procedure name | Purpose | Parameters |
---|---|---|
acbInitStack | Initialize the stack. | |
acbPushStack | Add an item to the stack. | A string to push |
acbPopStack | Remove an item from the stack. | |
acbCurrentProc | Retrieve the name of the current procedure. | |
acbGetStack | Retrieve a specific item from the stack. | The item number to retrieve |
acbGetStackItems | Retrieve the number of items on the stack. |
mastrStack, the array of strings that is
the stack itself; and mintStackTop, an
integer that holds the array slot into which the next stack item will
be placed. When you begin your work with the stack,
mintStackTop must be 0, so the first item
will go in the slot numbered 0. The acbInitStack
procedure does nothing other than initialize
mintStackTop:
Public Sub acbInitStack( )
' Resets the stack top to 0.
mintStackTop = 0
End Sub
You can add an item to the stack at any time by calling
acbPushStack . Pass to this subroutine the item
you want pushed. To push the item, the code places the item in the
array at the location stored in
mintStackTop and then increments the value
of mintStackTop. Its code is:
Public Sub acbPushStack(strToPush As String)
' Push a string onto the call stack.
' If the stack is full, display an error.
' Otherwise, add the new item to the call stack.
' Handle the error case first.
If mintStackTop > acbcMaxStack Then
MsgBox acbcMsgStackOverflow
Else
' Store away the string.
mastrStack(mintStackTop) = strToPush
' Set mintStackTop to point to the NEXT
' item to be filled.
mintStackTop = mintStackTop + 1
End If
End Sub
The only problem that might occur is that the stack might be full.
The constant acbcMaxStack is originally set to 20,
which should be enough levels. (Remember that
mintStackTop goes up one only when a
procedure calls another procedure. If you have 20 levels of procedure
calling, you might consider rethinking your application, instead of
worrying about procedure stacks!) If the stack is full,
acbPushStack will pop up an alert and will not
add the item to the stack.When leaving a procedure, you'll want to remove an
item from the stack. To do so, call the
acbPopStack procedure:
Public Sub acbPopStack( )
' Pop a string from the call stack.
' If the stack is empty, display an error.
' Otherwise, set the current item to be the
' next one to be filled in. If you're logging,
' send the information out to the log file.
' Handle the error case first.
If mintStackTop = 0 Then
MsgBox acbcMsgStackUnderflow
Else
' Because you're removing an item, not adding one,
' set the stack top back to the previous row. Next time
' you add an item, it'll go right here.
mintStackTop = mintStackTop - 1
End If
End Sub
Just as in acbPushStack , this code first checks
to make sure that the stack integrity hasn't been
violated; you can't remove an item from the stack if
there's nothing to remove! If you try,
acbPopStack will pop up an alert and exit. If
the stack is intact, the procedure will decrement the value of
mintStackTop. Decrementing that value sets
up the next call to acbPushStack so that it will
place the new value where the old one used to be.To retrieve the value at the top of the stack without pushing or
popping anything, call the acbCurrentProc
function:
Public Function acbCurrentProc( ) As String
' Since mintStackTop always points to the next item to
' be filled in, retrieve the item from mintStackTop - 1.
If mintStackTop > 0 Then
acbCurrentProc = mastrStack(mintStackTop - 1)
Else
acbCurrentProc = "
End If
End Function
This function retrieves the value most recently placed on the stack
(at the location one less than
mintStackTop, because
mintStackTop always points to the next
location to be filled). You can't look at
mastrStack yourself, because
it's local to basStackand
that's the way it ought to be.
Since the details of how the stack works are kept private, you can
replace basStack, using a different architecture for the stack data
structure, and the rest of your code won't have to
change at all.To retrieve more information about what's in the
stack, you can call acbGetStackItems , to find
out how many items there are in the stack, and
acbGetStack , which retrieves a specific item
from the stack. For example, write code like this to dump out the
entire stack (see subroutine D , which does just
this, in the basTestStack module):
Debug.Print "Stack items currently:"
For intI = 0 To acbGetStackItems( ) - 1
Debug.Print , acbGetStack(intI)
Next intI
The acbGetStackItems function is simple: it
returns the value of mintStackTop, because
that value always contains the number of items in the stack:
Public Function acbGetStackItems( ) As Integer
' Retrieve the number of items in the stack.
acbGetStackItems = mintStackTop
End Function
The acbGetStack function is a little more
complex. It accepts an item number (requesting item 0 returns the
item at the top of the stack) and calculates the position of the item
to retrieve. Its source code is:
Public Function acbGetStack(mintItem As Integer) As String
' Retrieve the item that's mintItems from the top of the
' stack. That is,
' ? acbGetStack(0)
' would return the same value as acbCurrentProc.
' ? acbGetStack(3) would return the third value from the top.
If mintStackTop >= mintItem Then
acbGetStack = mastrStack(mintStackTop - mintItem - 1)
Else
acbGetStack = "
End If
End Function
For the procedure stack to
work, you have to place calls to acbPushStack
and acbPopStack on entry and exit from every
procedure call. Good coding practice supports the concept of only one
exit point from each procedure, but even the best programmer
sometimes breaks this rule. To use the call stack, however, you must
catch every exit point with a call to
acbPopStack . Keep this in mind as you retrofit
old code to use this mechanism and when you devise new code to use
it. You can always code for a single exit point, and you will find
code maintenance much easier if you do.