Home
Introduction
Design
FSMs in VB
Event Queues
Data-Driven
Example
Real World
VB6 is Better

The Art of the State:
Data-driven Code

In an ideal implementation of a table-based design such as a finite state machine (FSM), the program is built from the tables themselves. In this kind of program, the tables are embedded in the code and somehow direct the flow of execution. The wisdom of this is clear: the tables are a product of our design process, and using them directly unifies the design – or at least some elements of it – with the code. It's also easier to make design changes because we don't have to translate between logical and physical models.

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, we 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, we can lay out the tables in compile-time initialisation statements. This is where the design and implementation intersect since we can lay out the table in a readable fashion and any changes we 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 we can do with it is pass the address of a function or procedure in a parameter list. Although we can use AddressOf in calls to Visual Basic functions, ultimately we 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 prior to version 5, but the fact that we can't invoke a Visual Basic function from an address means that we can't implement an action table like the C one shown above.

Or can we? We can certainly store Visual Basic function addresses in a table by passing them to a suitable procedure. Visual Basic permits us to store function addresses in long variables:

Sub AddAddressToTable(ByVal nState As Integer, _
                      ByVal nEvent As Integer, _
                      ByVal pcbVbCodeAddr As Long)
    ActionTable(nState, nEvent) = pcbVbCodeAddr
End Sub

Unfortunately, that's as far as we 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, we're on our own.

But don't despair, because we're not sunk yet. Visual Basic doesn't have a CallMe function, but there's nothing to stop us from writing your own. We'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. (We also need to use a DEF file to make the linker export the CallMe symbol.) Now all we need to do is include a suitable Declare statement in our Visual Basic code, and we can call Visual Basic functions from a table!

Declare Sub CallMe Lib "callme.dll" (ByVal lAddress As Any)
    ...
CallMe ActionTable(nState, nEvent)

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 we have to buy a compiler, and it adds an extra component to the distribution package we're going to have to build when we 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. We'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 we have to do is find an API function that takes a callback function, calls it once, and preferably doesn't do much else.

To cut a long story short, the convenient API function 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. We're not chaining any message handlers here, and we don't even have a window, but we can still invoke CallWindowProc to call a Visual Basic function. The only restriction is that our 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 permit us call a parameterless procedure as long as we trap the 'Bad DLL calling convention' error (error 49), but for reasons of portability – and good programming practice – we shouldn't rely on this.

The error 49 is generated when Visual Basic detects a stack frame anomaly on return from CallWindowProc, although there's nothing wrong with the API call itself. In fact, the error happens earlier, when the Action1 routine returns to CallWindowProc. CallWindowProc pushes four parameters onto the stack and expects the action function to remove them before it returns. (This is the Pascal, or stdcall, calling convention.) Our function doesn't oblige since it thinks there were no parameters, but because Windows 95/98 doesn't do stack checking at run time, the error goes undetected. Visual Basic does do stack checking, so it detects the error when the API call returns.

Windows NT is less forgiving here – it immediately detects the stack error and raises an exception inside CallWindowProc. You can't trap operating system exceptions in Visual Basic, so this kills the program (or kills Visual Basic if you're in the IDE). This, of course, is exactly what should happen, and it's a useful reminder that you shouldn't be taking such liberties!

All we need to do now is to wrap the CallWindowProc call up in a Visual Basic function, and we have a CallMe, effectively written in Visual Basic:

Sub CallMe(ByVal pcbAddress As Long)
    Call CallWindowProc(pcbAddress, 0&, 0&, 0&, 0&)
End Sub

Next...
 

Key Spinner

© 1998 - 2009 Mark Hurst. All rights reserved.   Updated March 01, 2009