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.
Figure A: Here's a typical application/DLL relationship.
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.
Defining the callback function
| Declaring a type for the callback function
| Writing the code that uses the callback
| |
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 __stdcallSo 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.
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.
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 #endifThis 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.
#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.
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.
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.
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.