When it comes to building data-driven programs, working with more traditional Windows programming languages such as C and C++ offers two definite advantages over Visual Basic. First, you can maintain tables of pointers to functions and invoke those functions directly through indexes into the tables. This removes the need for the unwieldy jumble of conditional statements needed in our first stab at an FSM in Visual Basic, reducing the DoFSM function to just two statements:
void fvDoFSM(int nState, int *nEvent) { (aapvActionTable[nState][*nEvent])(); nEvent = aanStateTable[nState][*nEvent]; }
Second, you can lay out the tables in compile-time initialization statements. This is where the design and implementation intersect since you can lay out the table in a readable fashion and any changes you make to it are directly changing the code. Here's what the comment stripper FSM tables might look like in a C program:
void (*aapvActionTable[NUM_STATES][NUM_EVENTS])() = { // E_SLASH E_STAR E_OTHER /* S_OUTSIDE */ {fvOutSlash, fvOutStar, fvOutOther}, /* S_STARTING */ {fvStaSlash, fvStaStar, fvStaOther}, /* S_INSIDE */ {fvInsSlash, fvInsStar, fvInsOther}, /* S_ENDING */ {fvEndSlash, fvEndStar, fvEndOther} }; int aanStateTable[NUM_STATES][NUM_EVENTS] = { // E_SLASH E_STAR E_OTHER /* S_OUTSIDE */ {S_STARTING, S_OUTSIDE, S_OUTSIDE}, /* S_STARTING */ {S_STARTING, S_INSIDE, S_OUTSIDE}, /* S_INSIDE */ {S_INSIDE, S_ENDING, S_INSIDE}, /* S_ENDING */ {S_OUTSIDE, S_ENDING, S_INSIDE} };
Unfortunately, although Visual Basic has an AddressOf operator, the only useful thing you can do with it is pass the address of a function or procedure in a parameter list. (C programmers will be disappointed to find that AddressOf isn't really like C's unary & operator.) Although you can use AddressOf in calls to Visual Basic functions, ultimately you can't do much inside those functions except pass the address on to a Windows API function. This capability is a major leap forward from all versions of Visual Basic previous to version 5, but the fact that you can't invoke a Visual Basic function from an address means that you can't implement an action table like the C one shown above.
Or can you? You can certainly store Visual Basic function addresses in a table by passing them to a suitable procedure. Visual Basic permits you to store function addresses in long variables:
Sub AddAddressToTable(ByVal niState As Integer, _ ByVal niEvent As Integer, _ ByVal pcbVbCodeAddr As Long) ActionTable(niState, niEvent) = pcbVbCodeAddr End Sub
Unfortunately, that's as far as you can go with pure Visual Basic. Perhaps a future version of Visual Basic will have a dereferencing operator or maybe a CallMe function that accepts an address and calls the function at that address; for now, however, you're on your own.
But don't despair, because you're not sunk yet. Visual Basic doesn't have a CallMe function, but there's nothing to stop you from writing your own. You'll need to write it in another language, of course, but if you're one of those Visual Basic programmers who breaks out in a cold sweat at the thought of firing up a C compiler, take heart-this is likely to be the shortest C program you'll ever see. Here's the program in its entirety:
#include <windows.h> BOOL WINAPI DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved) { return TRUE; } void CALLBACK CallMe(void (*pfvVbCode)()) { pfvVbCode(); }
The business end of this code is a single statement; the DllMain function is scaffolding to make a DLL. (You also need to use a DEF file to make the linker export the CallMe symbol.) Now all you need to do is include a suitable Declare statement in your Visual Basic code, and you can call Visual Basic functions from a table!
Declare Sub CallMe Lib "callme.dll" (ByVal lAddress As Any) . . . CallMe ActionTable(nState, nEvent)
The source code for the DLL and a Visual Basic program that calls it can be found in CHAP13\callme.
CallMe old-fashioned
The CallMe DLL is pretty simple, but it's still a DLL. It turns a programming project into a mixed-language development, it means you have to buy a compiler, and it adds an extra component to the distribution package you're going to have to build when you ship the product. Finding a way to do without a DLL would certainly be an attractive option.
Figuring out the answer simply requires a bit of lateral thinking. You've already seen how API functions that take a callback parameter can invoke Visual Basic functions, so it takes a simple shift of perspective to see such API functions as obliging CallMe servers. All you have to do is find an API function that takes a callback function, calls it once, and preferably doesn't do much else.
A quick trawl through the Win32 API documentation reveals SetTimer as a possibility since its sole purpose is to invoke an event handler that you register with it. The only problem with this is that SetTimer keeps calling the function until you kill the timer, so you must find a way to kill the timer after a single invocation. You could do this by including a call to KillTimer in the callback procedure itself, but this is ugly because the mechanism is inextricably bound up with the functions you want to call-if you're building an FSM, for example, all your action functions must look like this:
Sub Action1() Call KillTimer lTimerId ' Real action code goes here End Sub
The consequence of leaving out a call to KillTimer is a ceaseless barrage of calls to the offending function, with who knows what consequences-yuck!
There are other candidates, but one that works nicely is CallWindowProc. This function is normally used to attach a custom message handler (a.k.a. a window procedure) to a window; the custom message handler passes on unwanted messages using CallWindowProc, which tells Windows to invoke the default window procedure. You're not chaining any message handlers here, and you don't even have a window; but you can still invoke CallWindowProc to call a Visual Basic function. The only restriction is that your Visual Basic function must have the following interface:
Function Action1(ByVal hWnd As Long, _ ByVal lMsg As Long, _ ByVal wParam As Long, _ ByVal lParam As Long) As Long
Windows 95 and Windows 98 let you call a parameterless procedure as long as you trap the "Bad DLL calling convention" error (error 49), but for reasons of portability-and good programming practice-you shouldn't rely on this.
All you need to do now is to wrap the CallWindowProc call up in a Visual Basic function, and you have a CallMe, effectively written in Visual Basic:
Sub CallMe(ByVal pcbAddress As Long) Call CallWindowProc(pcbAddress, 0&, 0&, 0&, 0&) End Sub
Return of the comment stripper
It's time to return to the comment stripper. This time you're going to build a reusable FSM class using everything you've learned up to now-maybe you'll even pick up a few more tricks along the way. To see how the same FSM can be used to drive different external behaviors, you'll also make a slight modification to the program by displaying the text of the comments in a second text box. Figure 13-11 shows the new-look comment stripper.
Figure 13-11 Return of the comment stripper
First the bad news: you won't be able to match C's trick of laying out the FSM table readably in code. Visual Basic fights this on every front: you can't write free-format text, you run out of line continuations, there's no compile-time initialization, and even Visual Basic's comments aren't up to the job. However, this is the only bad news because using what you've learned about Visual Basic 6, you can do everything else the C program can do.
Let's start by looking at the interface to the FSM class. Since the class is to be general and you don't want to code the details of a particular FSM into it, you need to define methods that can be used to describe the FSM at run time. An FSM description will have four components: a list of states, a list of events, a table that defines state transitions, and a table that associates actions with the state transitions. In principle, the only other interface you need to the FSM class is a method you can call to feed events to the FSM. In practice, the restriction that demands that you put callback functions in a regular BAS file means you also need a method to register the event queue handler function with the FSM.
Here's what the run-time definition of the comment stripper FSM looks like:
Set oPiFSM = New CFSMClass oPiFSM.RegisterStates "OUTSIDE", "STARTING", "INSIDE", "ENDING" oPiFSM.RegisterEvents "SLASH", "STAR", "OTHER" oPiFSM.RegisterEventHandler cblEventQueueMessageHandler oPiFSM.TableEntry viState:="OUTSIDE", viEvent:="STAR", _ viNewState:="OUTSIDE", _ pcbiFunc:=AddressOf OutsideStar oPiFSM.TableEntry viState:="OUTSIDE", viEvent:="STAR", _ viNewState:="OUTSIDE", _ pcbiFunc:=AddressOf OutsideStar ' ...etc.
This code shows how the states and events are defined and also includes a couple of the table-definition statements. RegisterEventHandler creates a hidden window to act as the event queue and installs the cblEventQueueMessageHandler function as its window procedure. We'll look at the table definitions in a moment, but first let's examine the RegisterStates and RegisterEvents methods. These work identically, so we'll take RegisterStates as an example.
To make the class general, you need to be able to supply this method with a variable number of arguments. There are two ways to do this, but ParamArray is the best. The definition of RegisterStates looks like this:
Public Sub RegisterStates(ParamArray aviStates() As Variant) ' Some code here End Sub
ParamArray members are Variants, which is convenient in this situation because the FSM class will allow you to choose any data type to represent states and events. The example program uses strings, mostly because they're self-documenting and can be displayed on the form. In real applications, you might prefer to use enumerated types or integer constants. Without making any changes to the class definition, you could define your states like this:
Const S_OUTSIDE = 1 Const S_STARTING = 2 Const S_INSIDE = 3 Const S_ENDING = 4 . . . oPiFSM.RegisterStates S_OUTSIDE, S_STARTING, S_INSIDE, S_ENDING
Or like this:
Enum tStates Outside = 1 Starting Inside Ending End Enum . . . oPiFSM.RegisterStates Outside, Starting, Inside, Ending
Enumerated types were introduced in Visual Basic 5. In use they are equivalent to long constants defined with Const. Enumerations are better because they associate a type name with a group of constants, so in this example you can define variables of type tStates (although there is no run-time range checking). A more important difference is that you can define public enumerated types inside classes, which means you can now associate groups of constants directly with classes. If you were coding a comment stripper FSM class (instead of a general class that we'll use to implement the comment stripper), for example, you could define public tStates and tEvents as enumerated types in the class itself.
The FSM class can cope with any data type for its states and events because internally they are stored as integers and use collections to associate the external values with internal ones.
Here's the code behind RegisterStates:
Private Type tObjectList colInternalNames As New Collection colExternalNames As New Collection End Type Private tPiList As tObjectList . . . tPiList.colInternalNames.Add nInternId, key:=CStr(vExternId) tPiList.colExternalNames.Add vExternId, key:=CStr(nInternId)
This code creates two reciprocal collections: one storing integers keyed on external state names and the other storing the names keyed on the integers. You can now convert freely between internal (integer) and external (any type) states. Since you can store any data type in a collection, you are free to choose whichever data type is most convenient.
Tip
Using pairs of collections is a powerful way to associate two sets of values. Usually, one set is how the values are represented in a database and the other set is how you want to display them to the user.
The FSM table itself is created dynamically inside the RegisterStates or RegisterEvents routine (whichever is called last), using the Count properties of the state and event collections for its dimensions:
Private Type tTableEntry nNextState As Integer pcbAction As Long End Type . . . ReDim aatPiFSMTable(1 To nStates, 1 To nEvents) As tTableEntry
Now you need to fill in the empty FSM table with details of the state transitions and actions. To do this, you make repeated calls to the TableEntry method, with one call for each cell in the table. The values you want to insert into the table are successor states, which have one of the values defined earlier in the state list, and subroutine addresses, which you obtain with the AddressOf operator. The action routines are all parameterless subroutines, defined together in a single BAS file. Here's what the TableEntry method does:
aatPiFSMTable(nState, nEvent).nNextState = niNewState aatPiFSMTable(nState, nEvent).pcbAction = pcbiFunc
The nState and nEvent integers are first obtained by looking up the external names passed as parameters.
Once the table is in place, the FSM is ready to go. In fact, the FSM is running as soon as you define it since RegisterEventHandler creates an event queue and registers a callback function to service it. RegisterStates puts the FSM into its start state, but it won't actually do anything until you start feeding events to it.
The event queue is implemented as an invisible window created with Windows API functions as described earlier. The only minor problem here is that Visual Basic insists that you define callback functions in normal BAS files, so you can't include the queue event handler in the class definition. You can almost do it because you can define the event handler in the class as a Friend function; the function you register is a simple shell that calls the Friend function, although it still has to be in a normal BAS file. The class must contain the following function.
Friend Function cblEvHandler ( ByVal hwnd As Long, _ ByVal lMsg As Long, _ ByVal wparam As Long, _ ByVal lparam As Long ) As Long
This is a standard window procedure (don't forget the ByVals!), and you send events to it using the PostMessage API function. A Friend function is essentially a public method of the class, but the scope is limited to the current project even if the class is defined as Public. A call to PostMessage is the essence of the PostEvent method, and Windows arranges for the messages to be delivered asynchronously, via calls to the cblEvHandler function, in the sequence they were posted.
Calls to PostEvent are made in response to external stimuli, and in this case these are all Visual Basic keypress events. The calls are made from the KeyPress events, where the translation from ASCII code to an appropriate event value ("STAR", for example) is made. After the FSM is initialized, the KeyPress events are the only interface between the FSM and the outside world.
The queue event handler is the focus of the FSM since here is where the table lookup is done and the appropriate action procedure is called:
CallMe aatPiFSMTable(nPiCurrentState, wparam).pcbAction nPiCurrentState = aatPiFSMTable(nPiCurrentState, wparam).nNextState
The only other noteworthy feature of the queue event handler is that it contains calls to RaiseEvent. The FSM class defines four different events that can be used in the outside world (the comment stripper program in this case) to keep track of what the FSM is doing. These are the events:
Event BeforeStateChange(ByVal viOldState As Variant, _ ByVal viNewState As Variant) Event AfterStateChange(ByVal viOldState As Variant, _ ByVal viNewState As Variant) Event BeforeEvent(ByVal viEvent As Variant) Event AfterEvent(ByVal viEvent As Variant)
You saw an example of RaiseEvent earlier; this time, you're defining events with parameters. You define two sets of events so that you can choose whether to trap state changes and events before or after the fact. For the comment stripper, use the AfterEvent and AfterStateChange events to update the state and event fields on the form.