February 1999

Using callbacks in DLLs

by Kent Reisdorph

By far, the most common use of DLLs is where the calling program simply calls functions in the DLL. Sometimes, however, the calling application needs periodic information from the DLL while a process is taking place. For example, if the DLL is doing some lengthy calculations, it would be beneficial for the calling application to receive periodic reports of the calculation, such as the percentage complete. The calling application could use that information to display a progress bar or other indicator to the user. Callbacks allow you to do just that. This article will show you how to implement a callback function in a DLL.

What's a callback?

A callback is a function in an application that a DLL can call at suitable times. It's fairly obvious that an application can call functions in a DLL. This is the traditional relationship between a calling application and a DLL. Figure A illustrates the typical application/DLL relationship.

Figure A: Here's a typical application/DLL relationship.
[ Figure A ]

This figure is a little misleading, however, because it indicates that the data flows in only one direction--from the calling application to the DLL. The DLL can return data to the calling application in the form of a function's return value, but we won't be addressing that here. Right now we're more concerned with the dynamic passing of data from the DLL to the calling application. So now we've established that it's typical for an application to call a function in a DLL. Consider, then, the case of a DLL calling a function in an application. In addition, suppose that the DLL has no knowledge of the calling application or its functions. In fact, the DLL doesn't even know that the caller is an application at all--it could be another DLL. As you've probably already surmised, it is possible for a DLL to call a function in an application, provided that it has a pointer to such a function. Figure B illustrates a DLL calling a callback function in an application. Now that we've established what a callback is, we can move on to explaining how to implement a callback.

Figure B: A DLL calling a function in an application via a callback.
[ Figure B ]

Writing the DLL

The first step in implementing a callback is writing a DLL that will make use of the callback. This part of the job can be broken into three pieces:
bullet Defining the callback function
bullet Declaring a type for the callback function
bullet Writing the code that uses the callback
We'll examine each of these steps in detail in the following sections.

Defining the callback function

The DLL determines the function signature of the callback function. The DLL knows what information it needs to pass to the application, and it describes that information in the signature of the callback function. Let's take the example of a callback function that provides percent-complete information to the calling application. The function signature for this type of callback could be relatively simple:
void CALLBACK MyCallback(int percent);
As you can see, this callback returns void and has only one parameter--an integer. This parameter will contain the percent complete value, expressed as an integer between 0 and 100. Note the use of the CALLBACK macro in the preceding declaration. The CALLBACK macro for Win32 is defined in WINDEFS.H. The declaration looks like this:
#define CALLBACK __stdcall
So CALLBACK, when used in this context, is simply a function declared to use the __stdcall calling convention. It's not required that you use CALLBACK but it's a good idea if you want to enable developers using other development environments to use your DLL. Defining a function to use the CALLBACK modifier is something that other Windows programmers using your DLL will expect.

Declaring a type for the callback

Next, we must declare a new type for the callback function. This allows us to use the new type when we write the DLL function that will take a pointer to a callback function as a parameter. That probably doesn't make much sense right now, but we'll clarify it shortly. Given the prototype of the callback that we described earlier, the declaration of the new type would look like this:
typedef void CALLBACK(CallbackFunc)(int);
The typedef keyword associates the symbol name CallbackFunc with a pointer to a function. In this case the type has the signature described earlier. In other words, the CallbackFunc symbol is a new type that declares a pointer to a function, one that returns void and takes a single int for a parameter. You'll see how that type is used in the next section. Notice the use of the CALLBACK macro as described in the preceding section. This type definition should be placed in the DLL's header file.

Writing code that uses the callback

Now, we have an idea what the callback function should look like and a new type to describe that function. Next, we need to put the callback to use in the DLL. Here's an example of a function in the DLL, called CalcResults, that uses the callback:
void DLL_EXP CalcResults(CallbackFunc* Callback)
Should this be indented?
{
  for (int i=0;i<100;i++) {
    // do some processing here
    if (Callback)
      Callback(i);
  }
}
Let's take a moment to analyze this code. First, note that this function has a single parameter--a pointer to type CallbackFunc. It's easier to reference a function pointer by name, which is only possible when you've declared a type for that function.

Notice also the use of the DLL_EXP symbol in this code example. The symbol tells the compiler whether the function is being exported or imported. This symbol is defined in the DLL's header like this:

#if defined(__DLL__)
  #define DLL_EXP __export
#else
  #define DLL_EXP __import
#endif
This code defines DLL_EXP as __export when building the DLL and __import when building the calling application. The CalcResults function contains a loop that performs some kind of processing. The important part of the loop, for our purposes, contains this code:
if (Callback)
  Callback(i);
This code first checks the Callback variable for a non-zero value. We must check the value of Callback because the user could pass 0 for this value when calling the CalcResults function. If Callback happens to be 0, then attempting to execute Callback will result in an access violation. Your DLLs should be written in such a way that your users can opt to use the callback or not, as they see fit. If Callback is 0, you just don't attempt to call the callback function. If Callback is non-zero, then the next statement executes the callback function, passing the variable i as a parameter. Don't be concerned if you find this line of code a bit odd. It is indeed strange syntax. The fact is that this is how you call a function when that function is represented by a function pointer. In short, this code calls the user's callback function each time through the loop, provided the user has defined a callback function. Now that we have the DLL part of the equation done, we can move onto the calling application.

Writing the calling application

The code in the calling application is relatively simple. First, declare a function to be used as the callback:
#include "MyDll.h"

void CALLBACK MyCallback(int percent)
{
  // code here to do something with 
  //`percent'
}
There are two things to note about this function. First, note that the callback function is a stand-alone function and not a member of a class, such as the form's class. A callback function must be a regular function and can't be a class member function. (You could conceivably use a VCL event as a callback, but that would unnecessarily complicate the issue. In addition, the callback couldn't be used in non-VCL calling applications.) The second thing to note is that the function declaration for the callback exactly matches that of the CallbackFunc type declared earlier. Now that we've written the callback function, the only remaining item is to call the CalcResults function in the DLL. (This statement assumes that you've already added the import library file for the DLL to your calling application's project.) The code to call CalcResults is simple:
CalcResults(MyCallback);
Here, we call CalcResults, passing a pointer to the callback function, MyCallback, as a parameter. When this code executes, the CalcResults function will be called and, subsequently, the MyCallback function will be called at periodic intervals by the DLL.

Adding a return value

Many callbacks allow for controlling some aspect of the DLL's processing function via the callback's return value. For example, you might return true from the callback to continue processing, or false to stop processing. First, we need to modify the code in the DLL to account for the new callback signature. Here's the new declaration for the callback type:
typedef bool CALLBACK(CallbackFunc)(int);
Note that the return type is now bool instead of void as it was previously. Next, we need to modify the DLL's CalcResults function to utilize the return value from the callback. Here's how that function looks under the new mechanism:
void DLL_EXP CalcResults(CallbackFunc* Callback)
Indent?
{
  for (int i=0;i<100;i++) {
    // do some processing 
    if (Callback)
      if (!Callback(i))
        break;
  }
}
This code checks the return value from the callback function and, if false, breaks out of the loop. It's as simple as that. Note that we used the traditional C++ shorthand for an if statement in this example. The long version would look like this:
if (Callback(i) == false)
  break;
Obviously some changes need to be made to the callback function in the calling application, as well. Going back to our example, let's say you wanted to stop processing when the percent completed reaches 60 percent. In that case, the callback function in the calling application might look like this:
bool CALLBACK MyCallback(int percent)
{
  // do some things
  if (percent < 60)
    return true;
  else
    return false;
}
This is the traditional notion of "return true to keep processing, or false to stop." Granted, not all processes need to be interruptible, but some do, and this is the method you should use for those cases.

Adding a user data parameter

There's one other technique that's commonly implemented in callback functions--the user data parameter. A user data parameter allows you to send any data you wish to the callback function. The user data parameter is almost invariably a DWORD. (For those of you not familiar with Windows API programming, a DWORD is an unsigned integer.) Why a DWORD? Because a DWORD is four bytes in size and can ultimately be used to pass any amount of data to the callback function. You can, for example, cast a pointer to a DWORD, thereby passing just about any amount of data to your callback function. Once again, let's modify the callback mechanism to add a user data parameter. First, let us show you the callback type definition:
typedef bool CALLBACK(CallbackFunc)(int, DWORD);
As you can see, a DWORD parameter has been added to the callback declaration. Now let's look at the modified CalcResults function in the DLL:
void DLL_EXP CalcResults(
  CallbackFunc* Callback, DWORD UserData)
{
  for (int i=0;i<100;i++) {
    // do some processing
    if (Callback)
      if (!Callback(i, UserData))
        break;
  }
}
Note that the CalcResults function now takes a DWORD as a parameter. Note, also, this line:
if (!Callback(i, UserData))
All we're doing here is passing the user data parameter of the CalcResults function straight through to the callback function. Our responsibility is to pass on the user data without modifying it in any way. In the calling application, we'll pass the form's pointer to the callback function in the user data parameter. Here's how the call to CalcResults looks with that modification:
CalcResults(MyCallback, (DWORD)this);
We're passing the pointer to the MyCallback function as we did before, but we're also passing the form's pointer. (Remember, the this keyword, when used within a class, is a pointer to the class itself--the form in this case.) As indicated earlier, the pointer must be cast to a DWORD because both the CalcResults function and the callback function take a DWORD for the user data parameter. Finally, let's look at the new implementation of the callback function in the calling application:
bool CALLBACK 
MyCallback(int percent, DWORD userData)
{
  TForm1* form = (TForm1*)userData;
  form->ProgressBar1->Position = percent;
  return true;
}
Remember, we passed a pointer to the form when we called the CalcResults function. Now we simply cast the userData parameter back to a TForm1* so we can access members of the form class. We then use this pointer to update the status bar on the form with the percent complete. Note that we're unconditionally returning true so that the processing runs to completion. If you always provide a user data parameter in your callbacks, your users will have all the functionality they need (in almost any circumstance). Adding a user data parameter is simple and makes your DLLs more useable and more professional.

Conclusion

Listing A shows the header for our test DLL, Listing B shows the source for the DLL, and Listing C shows the source for the calling application.

Listing A: CBDLL.H

#ifndef CBDLL_H
#define CBDLL_H

#if defined(__DLL__)
  #define DLL_EXP __export
#else
  #define DLL_EXP __import
#endif

// Type definition for the callback function.
typedef bool CALLBACK(CallbackFunc)(int, DWORD);

// Declaration for the CalcResults function.
extern "C" {
  void DLL_EXP CalcResults(
    CallbackFunc* Callback, DWORD UserData);
}

#endif
Listing B: CBDLL.CPP
#include <windows.h>
#pragma hdrstop

// Include the DLL's header.
#include "cbdll.h"

// The standard DLLEntryPoint function.
int WINAPI DllEntryPoint(
  HINSTANCE, unsigned long, void*)
{
  return 1;
}

// Our CalcResults function.
void DLL_EXP CalcResults(
  CallbackFunc* Callback, DWORD UserData)
{
  // A loop to simulate processing.
  for (int i=0;i<100;i++) {
    // Delay a bit to simulate some process.
    Sleep(50);
    // If a callback was provided, call it.
    if (Callback)
      // Pass i and userData. If the callback
      // returns false, stop processing.
      if (!Callback(i, UserData))
        break;
  }
}
Listing C: TESTAPP.CPP
#include <vcl.h>
#pragma hdrstop

#include "TestAppU.h"
// Include the DLL's header.
#include "cbdll.h"

#pragma resource "*.dfm"

TForm1 *Form1;

// Our callback function.
bool CALLBACK
MyCallback(int percent, DWORD userData)
{
  // Cast userData back to a TForm pointer.
  TForm1* form = (TForm1*)userData;
  // Update the status bar.
  form->ProgressBar1->Position = percent;
  // Return true to keep processing.
  return true;
}

__fastcall TForm1::TForm1(TComponent* Owner)
  : TForm(Owner)
{
}

void __fastcall 
TForm1::Button1Click(TObject *Sender)
{
  // Call CalcResults, passing the address of
  // the callback function and a pointer to the
  // form as parameters. Must cast 'this' to a
  // DWORD to make the compiler happy.
  CalcResults(MyCallback, (DWORD)this);
  ProgressBar1->Position = 0;
  Label1->Caption = "Done!";
}
The calling application is simply a form with a button, a progress bar, and a label. When you click the button, the CalcResults function is called and the callback function updates the status bar. The CalcResults function doesn't actually do any processing (other than impose a short delay), but it does illustrate the concepts presented in this article.

Kent Reisdorph is a editor of the C++Builder Developer's Journal as well as director of systems and services at TurboPower Software Company, and a member of TeamB, Borland's volunteer online support group. He's the author of Teach Yourself C++Builder in 21 Days and Teach Yourself C++Builder in 14 Days. You can contact Kent at editor@bridgespublishing.com.