Handling Callbacks Between Legacy Visual BASIC and DLLs

Home

Back

Downloads

Sample Code Project

Sample Compiled Binaries

 

 

 

 

Few people realise the degree to which legacy Visual BASIC 5 and 6 uses or exposes pointers in ways which can be used as in lower-level languages such as "C". It is perfectly feasible to do advanced things in VB which would normally require pointers, Callbacks being one of them. The developers of VB simply went to great lengths to hide pointers from the programmer to prevent them being used. If you develop DLLs then understanding pointers is essential.

For those unfamiliar with the concept a "callback" permits one routine to call another. The term "callback" is probably a misnomer since the block of code doesn't necessarily call "back" to the same function at all; but a secondary section of code is permitted to call "out" to somewhere else within your program. This may include calling back into the original current routine but it need not, it could call "back" anywhere where a valid address can be supplied. Typical uses for a callback would be some form of progress counter provided during a very time-consuming task such as compression or searching. The callback function might perform checks or update a visual status such as a progress-bar. The external code can call *any* compiled VB code it has a valid pointer for.  "C" programmers will be familiar with callbacks if they use the "C" library quicksort (qsort) function where the compare function is actually a callback to user defined comparison code.

The following topics are covered below:

  • The use of pointers and the AddressOf keyword in VB
  • How VB handles the String data type
  • How "C" routines such as DLLs can handle VB-compatible strings
  • The difference between VB and "C" integer data types
  • The appropriate use of _stdcall (or __stdcall) when interfacing to VB
  • Creating a complex function pointer declaration in "C" which matches a VB procedure
  • Example code in VB and "C"

You will need a basic understanding of "C" and VB to benefit from this article. The tutorial doesn't cover writing Windows DLLs in depth - there are plenty of tutorials available on the subject. VB code is enclosed in a box with a white background and sample "C" code appears on a dark-blue background

What is a Pointer?

Far from being pointless, a pointer is simply a variable or location in memory which can hold a reference to a memory-address. You can have pointers to any object you can store in memory and this also includes functions (or subroutines). An unfamiliar concept for VB programmers is that when you de-reference a pointer in "C" you actually perform the function call. Also, since a raw record of a memory address carries no useful descriptive information about the function's parameter-list or return-type it is necessary to create a proper pointer-declaration in "C" so that parameters are passed correctly and the compiler calls the function in the right way. This is done for you automatically when you declare and call functions natively within the compiler but if you use pointers there is a little more "leg-work" for you to do.

void funct()   // A simple function called directly in "C" 
{
               // This function doesn't do anything useful
}

void (foo)();  // A pointer called "foo" which is a pointer to a function which takes 
               // no parameters and returns nothing and which matches funct()
               // "foo" is recognised as a pointer by virtue of the enclosing brackets ()
               // The compiler now "knows" how to use foo as a function-pointer

foo=funct();   // Make foo point to funct()

*foo;          // Call funct() by dereferencing the pointer called "foo"

VB5 introduced the AddressOf operator which reveals the address of a function and which gives us our leverage into using function-pointers to VB code. "C" programmers will be perfectly comfortable with the idea of function addresses and function casts - but these will be rather alien to VB programmers so they require a little more explanation. The AddressOf feature allows a programmer to pass information about the memory-address of a Visual BASIC routine to a block of "C" code such as a DLL which can then allow that code to call the specified VB routine. This may be in the original function or sub or it may call some other, completely different block of VB code. Many Windows API routines require the address of a callback function and these can work perfectly with a VB subroutine.

The problem one might encounter with callback has more to do with understanding function pointer casting and the data-type differences between VB and "C". Particularly how VB stores and passes the String data type. Once you can grasp these concepts using callbacks isn't that hard at all.

Visual BASIC Strings Are Unicode

The most challenging data type to manipulate is the VB String type due to automatic conversion by VB. Also VB stores strings internally as Unicode or double-byte/16-bit characters and the data-structure used to store them is not a simple array as in "C" but an OLE data type called a BSTR. These aren't particularly complex data-types other than they store the length of the string whereas "C" strings are raw arrays of bytes which store no length value. This absence of length information is one potential source of vulnerabilities in "C" programs which can lead to buffer-overflow exploits. Actually, Unicode is a bit more complex than that but just take all this as a fact for now and if you want to know more then read this article.

On the "way out", i.e. sending strings from VB to "C" DLLs VB automatically converts strings to ANSI; so receiving a string from VB within external "C" code is less of a problem. You can declare a char* or const char* within your external "C" code and get the data you expect in a nicely-converted ANSI string. Sending strings "back" to VB from an external DLL via a callback is more problematic as passing data via this route wasn't anticipated or provided for within the VB design so you will have to use the correct data type. You also have to ensure your function pointer declaration matches the VB procedure definition. You will also take responsibility for memory-allocation and cleanup for strings as well as ensuring pointer-scoping is valid.

Strings which are passed back to VB via a callback *must* be declared as type BSTR  (unsigned short*) within your "C" code. These are arrays of 16 bit characters in the same way that strings within "C" are simple arrays of 8-bit characters. However, there is a difference; the BSTR data type has a data structure which includes a string-length header and they have a double null (\0\0) terminator sequence. You don't need to worry about the data-structure though - just acknowledge it is there and that VB will be able to properly handle any BSTR strings you send back to it.

An 8-bit ANSI "C" char string looks like this when displayed:

"Hello World"           'Hex=48656C6C6F20576F726C64

whereas a 16-bit wide/multibyte or Unicode string looks like this when displayed...

"H e l l o  W o r l d " 'Hex=480065006C006C006F00200057006F0072006C006400

Each character is represented by 16 bits or two bytes with every 2nd byte usually being zero for Western alphanumerics.

Data-Route from Visual BASIC Visual BASIC Declare "C" Declare
Visual BASIC to "C" (outwards) String char* or BSTR
"C" to Visual BASIC (return) String BSTR

You can allocate BSTRs in the same way as arrays of char within "C" code and similar rules of memory allocation and scoping apply. For callbacks you will be allocating memory and then calling "out" to some external code which must be able to translate any pointer reference to a block of allocated-memory. For this reason care should be taken that any string pointers actually point to valid/allocated memory or both blocks of code could hang or crash.

Strings which are returned back to VB by other than a callback functions need not be declared as BSTRs. i.e. if you are developing a DLL which simply receives a string, processes it and then returns it to VB then you can return an ANSI string and VB will perform the necessary conversion. Also note that the BSTR data type can be used to hold an ANSI (non-wide) string. Here, though, we are only concerned with sending strings back to VB via the complication of a function pointer which has to have a prototype properly declared in your "C" code - and this must match the BSTR data type when strings are used.

Allocating BSTR Strings

To allocate memory for BSTR strings within "C" code to send back to VB you can use the SysAllocString Windows API function. Deallocating is performed using SysFreeString  You can also use static or automatic variables but any memory which is allocated within your code must be released before pointers are re-used or goes out of scope. When using string literals the "L" keyword may be used to specify a multibyte const string literal to the compiler. For example...

BSTR ptr=SysAllocString(L"Hello World");

This declares ptr to be a BSTR (or unsigned short*); memory is then allocated using SysAllocString using a wide string-literal. You should test the return from SysAllocString against NULL since this value is returned should the function fail. You *must* supply SysAllocString with a wide-character string (Unicode) on 32-bit systems.

SysFreeString is used to free any BSTR previously allocated by a compatible API function. The callback function-call would occur between the SysAllocString call and the SysFreeString call should this string be passed back to VB.

SysFreeString(ptr);

Passing Numbers

Numeric data types are much easier to pass-around between VB and external "C" code. Using these is simply a matter of recognising and using the correct data-type equivalent within "C" which matches the one in VB. One data-type which can cause severe problems however is the Integer data type.

Passing Integers

The most important fact to note is that within the "C" language the integer data type can vary and is "system-dependent". Worse still, the "int" data type may be the default one. In other words there's no way to guarantee how many bits an integer data-type will occupy when dealing with different systems and interfaces and not being precise can lead to bad-habits and unexpected or difficult to debug errors. More importantly the data-width for the VB and "C" integer types is completely different. VB type Integer is 16 bits wide whereas the "C" int data type will be 16 bits on Win16 (Win3.x/9x), 32 bits on Win32 and possibly 64 bits on Win64 systems. Our solution lies in the short data type. The short type is guaranteed to be always 16 bits and thus match Visual BASIC's Integer type. All of this matters if you are creating interfaces which call functions and want them to work. Get this wrong and, again, you will have code crash and break.

Table of Data Type Equivalents Between VB and "C"

VB Data Type Data Width in Bytes Bits "C" /"C++" Equivalent API Equivalent Native Alternative Comments
String Unlimited or system-dependent - BSTR OLECHAR unsigned short* BSTR is a 32-bit pointer
Byte 1 8 char BYTE - -
Boolean 2 16 short - - Although 16-bit these can store only 0 (false) or -1 (true)
Integer 2 16 short WORD - -
Long 4 32 long DWORD - -
Double 8 64 double - - -
Date 8 64 double - - Custom data-format
Type Varies - struct - - UDTs
Variant Varies - - - - Complex OLE data-type


Callbacks and Pointers

So far we've looked at the complications of ensuring that the data-types used by "C" code match the expectations of Visual BASIC and taking care that we don't cause our application to crash due to a mismatch due to using inappropriate types. Now we can look at how pointers are used and how our "C" code uses a function-pointer to call VB code.

First of all, getting the address of our VB Sub or Function is simplicity itself. One simply uses the AddressOf keyword. However, VB imposes severe limits on where you can use AddressOf and restricts it's use as a data-type modifier to function-calls. This means we need a workaround in order to "grab" the address within the limitations of the VB syntax. For this we can use a simple "one-line" VB wrapper-function called GetProcAddress() as shown below...

Public Function GetProcAddress(ByVal Addr As Long) As Long 
  ' Return a procedure's address 
  ' Call as x=GetProcAddress(AddressOf Procedure) 
  GetProcAddress = Addr 
End Function

We can't use AddressOf in the immediate pane to test our code but we can create a small test Sub to check out that this is working OK

Public Sub Test()
  Dim l As Long
  l = GetProcAddress(AddressOf GetProcAddress)
  Debug.Print "AddressOf GetProcAddress is: "; l
End Sub

This will return something like - "16137468" as the address of GetProcAddress itself. Thus we can confirm to ourselves that we can grab the address of any Sub or Function in VB. Pointers are data types suitable for holding a valid machine-address.

Now we have a pointer we can pass to "C" code we have to deal with the problem of creating the function interface which will match the address. We can't just give the "C" routine a bare address or it will simply not know how to handle any parameters which are passed onto it and any mismatch will cause a crash. (parameters are usually pushed on the stack). Here we have to deal with creating a "C" function pointer declaration

In "C" and C++ any value which can hold a machine-address and which can be de-referenced can be used as a function-pointer. On Win32 systems the long data type will  be capable of doing the job. But we don't actually want to deal with a simple pointer, we will be needing to create a function prototype which will precisely match the VB code we will be calling. Get this wrong and again we will have a crash.

Pointer declarations are defined in "C" by wrapping the variable name in brackets; so, if we have varname then (varname) within a declaration defines it as a pointer. "C" is a compact language which re-uses symbols with different meanings in different contexts. Normally one would be familiar with using brackets for operator precedence but understanding that brackets can have this different meaning when used in a different context.

Hence - int (foo) () is a pointer to a function called "foo" which takes no parameters and which returns an integer whereas int foo() would simply be a  simple function-declaration. If we wanted to declare a function called "foo" which returned a char and which took a char and a long as parameters we would declare char (foo)(char, long)

We want to declare function-pointers which return and take data-types which match VB ones. To recap, for VB String data types we use BSTR, for VB Integers we use short. So, for a function called "foo" which returned a VB Integer and took a String as the parameter we would declare the following as a raw function prototype.

short (Foo)(BSTR);

The matching VB declaration would be a Function in VB would be:

Public Function Foo(s As String) As Integer

As another example, here's a function which takes a Byte, an Integer and a String and returns a String:

BSTR (Foo)(char, short, BSTR);

The matching VB declaration in this case would be:

Public Function Foo(b As Byte, i As Integer, s As String) As String

Okay you might say, "what about Subs rather than Functions?" In such cases we take care to declare the return type as void in our "C" code. The code below is a sub which takes no parameters at all and returns nothing (the keyword void in "C" means undefined or, for the purposes of this tutorial, "nothing", "nada", "zilch"):

void (Foo)(void);

The matching VB equivalent declaration as a Sub (a Function with no return) would be:

Public Sub Foo()

If we added an Integer as a parameter to this we would get:

void (Foo)(short);

- with the VB equivalent:

Public Sub Foo(i As Integer)

You should now have a rough understanding of declaring a function pointer prototype within "C" which matches a VB procedure. Now we need to implement this in a "C" DLL which can make use of it. 

Below is a "C" DLL function declaration which takes a pointer to a Sub as it's argument, which in turn, takes a VB string and a VB Integer as it's argument. In this case remember we will be passing data back to VB and not receiving it from it. The _stdcall keyword added to the definition for "Foo()" tells the "C" compiler how variables are passed and how the stack is cleaned up after calling; just accept that this is necessary to ensure the DLL handles the stack properly. If you fail to declare the callback function as _stdcall then you will get a message saying "Error accessing memory location xxxx unable to write ... " etc. on exiting the function and and your code will crash.

void _stdcall Foo(void (MyProc)(BSTR, short))
{
  // Todo - add our code - Note that "C" function names are case-sensitive 
}

The above declaration has a major problem though. Since "MyProc" is calling VB and VB uses _stdcall or Win32-API calling conventions we also need to ensure "MyProc"  uses _stdcall. If we fail to declare our callback as _stdcall then the stack probably won't be cleaned up properly and we may get a GPF error depending what types of variables we happen to pass back to VB. Since this might not actually happen this would be an unpredictable and hard-to-trace error.

You can demonstrate this by downloading the sample project and removing the _stdcall from the MyProc declaration. You may find the program will run fine in the IDE but the compiled EXE will return a GPF.

Therefore, we must add the _stdcall as in the following code: Notice that this 2nd _stdcall goes inside the cast brackets for (MyProc):

void _stdcall Foo(void (_stdcall MyProc)(BSTR, short))
{
  // Todo - add our code - Note that "C" function names are case-sensitive 
}

Calling this function is now a simple matter of translating from function-declaration into an actual function call which does something useful. To do this we just remove the wrapping brackets around the function-name and insert actual variables to call MyProc() as follows:

void _stdcall Foo(void (_stdcall MyProc)(BSTR, short))
{
  // Todo - declare variables s and i
  MyProc(s,i);  // Perform our callback
}

This still won't work yet as we don't have the actual callback variables declared, but pulling-in information covered right at the start we can now do this ...

void _stdcall Foo(void (_stdcall MyProc)(BSTR, short))
{                                    // MyProc is a Sub taking a String & Integer
  BSTR s=SysAllocString(L"Oh hai!"); // Declare and allocate multibyte string
  short i=42;                        // Our Meaning of Life variable
  MyProc(s,i);                       // Perform callback and call the VB routine
  SysFreeString(s);                  // Release memory we allocated (most vital!)
}

Note that the function name has to be "exported" or made visible from the DLL to other applications and that exports are case-sensitive. Thus we use "MyProc" and not "myproc" or "MYPROC". Exporting DLL functions using a DEF file isn't covered here but you can read about it here. We change the case of the exported procedure name to ensure we match our final VB declaration and we MUST pass variables into the callback procedure "ByVal" (by value) and not ByRef (by reference) unless we specifically wrote our DLL to work this way using pointers to pointers.

The accompanying "C" DLL project DEF file would look something like this assuming that we compile the project with the name "vbcallback". Comments are prefixed with a semi-colon (;) ...

LIBRARY vbcallback

DESCRIPTION "VB Callback Test Code"

EXPORTS
   ; Note that exports are case-sensitive
   Foo

Our DLL function now matches the following VB code which it is going to to "call-back" to. thus:

Public Sub MyProc(ByVal s As String, ByVal i As Integer)

So we can finally compile our DLL, place it somewhere in our system's PATH location such as C:\WINDOWS\ and move on to the Visual BASIC side of the project

Linking VB to the External DLL

We "connect" Visual BASIC to our external DLL by means of a "Declare" statement. This lets VB know about the external DLL and sets up a "function prototype" so it understands how to call and interface with the DLL. This is doing the equivalent of what we did within "C" when we created our pointer declaration prototype for "MyProc". Since we are passing only an address parameter which tells us where our callback function is located in memory then for this we will use:

Public Declare Sub Foo Lib "vbcallback" (ByVal Pointer As Long)

Finally, and of vital-importance we MUST call external DLLs using "ByVal" for argument values unless we will be handling pointers to numbers or pointers to pointers to strings. Another potential source of bugs is omitting the "ByVal" keyword altogether and calling "ByRef" (by reference) accidentally. This would pass a pointer to a pointer for say String data types and we would be reading the wrong memory location when we de-referenced pointers within our DLL.. And we wouldn't want to find ourselves in the middle of invalid memory - would we?

Public Declare Sub Foo Lib "vbcallback" (Pointer As Long)       ' WRONG


Putting It All Together

At this point we have a DLL function which does the following:

  • It declares a variable as a properly-formed function and exported to match the case and prototype of the VB one
  • It correctly allocates memory which is "in-scope" within the DLL and passes them back to VB in the correct way
  • It has a VB declaration to the compiled DLL in the correct format
  • It declares a VB-compatible String variable as BSTR
  • It declares a VB-compatible Integer as a short
  • It calls the external VB function as a callback
  • It correctly passes the address of our VB callback function using a Long data type which matches the raw 32-bit address type in "C"
  • ByVal are used rather than ByRef (since we're not using pointers to string pointers or pointers to numbers)
  • It correctly declares _stdcall where required to ensure variables are passed in the correct order and the stack is cleaned-up properly

All we need to do now is call the DLL from VB and have the DLL then call some VB code back. This is quite easy - we create the callback Sub and some test code to call it. We can even add a Static variable to count how many times the callback function has been called from the DLL

To test all of this we run TestCallback from the immediate pane in VB:

Option Explicit

Public Declare Sub Foo Lib "vbcallback" (ByVal Pointer As Long)
Public Sub TestCallback()
  Dim LongVal As Long
  Dim i As Integer
  LongVal = GetProcAddress(AddressOf MyProc) 
  Debug.Print "Test address is "; LongVal 
  For i = 1 To 10  ' We will call our external function 10 times
    Foo LongVal    ' Call the DLL with the procedure-address of MyProc()
  Next 
  End              ' Release the DLL End Sub
End Sub

Public Sub MyProc(ByVal s As String, ByVal i As Integer)
  ' The callback function itself called from "C" 
  On Error Resume Next	' Ensure errors are handled locally
  Static Count As Integer
  Count=Count+1
  Debug.Print "Callback called: "; Count; " time(s)"
  Debug.Print "Entered VB callback code"
  Debug.Print "DLL Parameter s is ["; s; "]"
  Debug.Print "DLL Parameter i is ["; i; "]"
End Sub

Public Function GetProcAddress(ByVal Addr As Long) As Long 
  ' Return a procedure's address 
  ' Call as x=GetProcAddress(AddressOf Procedure) 
  GetProcAddress = Addr 
End Function


Conclusion

Hopefully this provides a reasonably painless introduction into callbacks, function declaration and compatible data-types between legacy Visual BASIC and Win32 DLLs. Using callbacks and pointers isn't hard once you learn the ground rules and can see where common pitfalls are. You can download the example project and try modifying it for your own use.

Some Things Which Can Go Wrong - A Reminder

A quick list of the mistakes which are likely to result in a crash. Avoid these and all should be well. Some errors will cause a failure to compile, the worst ones will compile OK but result in wholly-unpredictable behaviour.

  • Failing to explicitly declare ByVal for procedure and interface arguments
  • Incorrect external DLL "Declare" formed within VB
  • Failing to allocate pointers within "C" DLL code
  • Passing invalid, NULL, non-initiated or "out-of-scope" pointers back to VB
  • Failing to handle outbound "C" strings being passed to VB as BSTR data types
  • Failing to allocate memory for strings being passed to VB
  • Failing to declare DLL functions as _stdcall
  • Failing to declare callback functions as _stdcall
  • Putting the callback function _stdcall declaration in the wrong place (outside the cast brackets)
  • Forgetting to wrap the callback function name parameter in a function-pointer "cast" with suitable brackets
  • Confusing function prototypes for VB Sub and Functions, returning values to Subs or failing to return values to Functions
  • Using the "C" int data type to interface with the VB Integer data type
  • Using the "C" int data type to interface with the VB Boolean data type
  • Returning 1 for True to a VB Boolean instead of -1 for True

Download Example Source Code and Binaries

Further Reading

Podcasts and Video

 

v1.01 - Copyright (C) M Shaw - August 2010 - Updated 06 March 2021 - Code was coloured statically using CGIHighlight