One of the most important steps in the development process is the selection of the programming language with which an application will be developed. This task frequently presents the developer with a very difficult decision. The reason for this is that several programming languages may have good features that the developer would like to implement. However, the developer does not have to select a language with one set of features to sacrifice another. Through the use of DLLs (Dynamic Link Libraries), the developer can have the benefit of many different languages and all the features they offer. The development task can be divided into several modules; each of which can be developed in a different language. This tutorial will instruct the reader on how this task is accomplished using two of the most popular programming languages: C++ and Visual Basic.
Creating an ActiveX DLL in Visual Basic
The DLL
Creating a DLL in Visual Basic is very similar to creating a class module. The only difference is that some additional overhead is required for compiling and referencing the DLL from the client application. To create the DLL, complete the following steps:
- Open Visual Basic and select new ActiveX DLL.
- Rename the Class and Project to reflect the name that the client application will use in its code.
- Add any member functions as you would to a normal class module.
One important part of the DLL is the initialize and terminate functions. These are called when the DLL is first loaded into memory and also when it is removed. The following code illustrates the use of both functions:
Code: Select all
' Initializes the Class
Private Sub Class_Initialize()
MsgBox "This procedure is called when the DLL is loaded into memory.", vbOKOnly, "Initializing Class..."
End Sub
' Terminates the Class
Private Sub Class_Terminate()
MsgBox "This procedure is called when the DLL is removed from memory.", vbOKOnly, "Cleaning up..."
End Sub
Before a DLL can be accessed by a client application, Visual Basic must make a reference to it. This is done by clicking Project -> References. If the DLL is not in the list, select browse to indicate where the file is located. If the EXE is intended to be a test application for debugging a DLL, select the project file for the DLL. This feature makes it easy to create a DLL because it does not need to be recompiled as often.
At this stage, the client application can use the ActiveX DLL as though it were a standard class module, located within the application itself. The following is the line of code required to declare an instance of the class and call its initialization member function:
Code: Select all
Set Library = New testDLL
Calling member functions is exactly the same as a standard class module:
Code: Select all
Call Library.Basic_Sub_Call
Creating a C++ DLL
The DLL
DLLs are more difficult to create in Visual C++ than in Visual Basic, but they are still relatively simple. There are two methods with which to link to a DLL: with or without an Import Library. Using an Import Library is much easier if the target language is a C++ application, however, Import Libraries are not used when linking from Visual Basic, so they will not be covered here.
Using an Import Library can also reduce the flexibility of both the DLL and client application, but more overhead is required to ensure it functions properly. This is especially true for applications that use plugins because the Import Library would be linked in with the final executable of the client, thus eliminating the ability to add new plugins to an application. In order to create a simple DLL, follow these initial steps:
- Open Visual C++ and select New Win-32 Dynamic Link Library.
- Select the radio button labeled A Simple DLL Project.
- Click Project -> Add to Project -> New… -> Text File
- Name the file the same as your module with the .DEF extension (e.g. testDLL.def).
In the main module (testDLL.cpp in the example), make sure the windows.h header file is included. Once this is complete, functions may now be added to the DLL. However, there declaration is slightly different than a normal C function because of the way function calls are handled in Visual Basic:
Code: Select all
void __declspec(dllexport) CALLBACK TestFunc()
{
cout << "Inside the DLL!";
}
Code: Select all
LIBRARY "testDLL_Library"
DESCRIPTION "An example DLL for interfacing with C++"
EXPORTS
TestFunc @1
RetInt @2
OtherFunc @3
The DllMain Function
This function is called during various times of the DLL’s existence in memory. There are four possible times when it is called:
DLL_PROCESS_ATTACH - Indicates that the DLL has just been loaded into memory.
DLL_THREAD_ATTACH - Indicates that the client application is creating a new thread (not covered).
DLL_THREAD_DETACH - Indicates that a thread is exiting (not covered).
DLL_PROCESS_DETACH - Indicates that the DLL is being unloaded from memory.
When DllMain is called, the ul_reason_for_call argument will hold one of these four values. It is usually a good idea to create a simple case statement and test for each of these events.
The function must return true if it successfully accomplished its task or false if an error was produced. In the event of an error, Windows automatically stops loading the DLL and produces an error message. This is only accounted for when the DLL is first loaded; the return value is ignored when it is unloaded from memory.
The Client Application (C++)
Setting up the client application can be very tedious for large DLLs. This is why developers should include a header file, which defines the function prototypes and properly loads them into memory. However, for simplicity this will not be done in the example code. The first task is to define a typedef for every function in the DLL. Several examples follow:
Code: Select all
typedef void (WINAPI*cfunc)();
typedef int (WINAPI*ifunc)(int t);
Next, pointers must be declared for each function; just as they were done for the typedef statements. This is done just like a normal variable would be declared:
Code: Select all
cfunc TestFunc;
ifunc RetInt;
Code: Select all
HINSTANCE hLib = LoadLibrary("testdll_library.dll");
if(hLib == NULL)
{
cout << "ERROR: Unable to load library!" << endl;
getch();
return;
}
Finally, the client application must search through the DLL and find the address of each function within the DLL’s memory. This can be done by calling the GetProcAddress function:
Code: Select all
TestFunc = (cfunc)GetProcAddress((HMODULE)hLib,"TestFunc");
RetInt = (ifunc)GetProcAddress((HMODULE)hLib,"RetInt");
Before the client application terminates, it is important to unload the DLL from memory. This is accomplished by calling FreeLibrary:
Code: Select all
FreeLibrary((HMODULE)hLib);
Accessing a C++ DLL from Visual Basic is much easier than from C++. The only task that must be done is to declare the function prototype; Visual Basic will handle everything else. In order to do this, include a code module by clicking Project -> Add Module -> Open. In the new module, declare the function prototypes in the following manner:
Code: Select all
Declare Sub TestFunc Lib "../testdll_library.dll" ()
Declare Function RetInt Lib "../testdll_library.dll" (ByVal t As Integer) As Integer
The following table illustrates the equivalent conversion between several simple data types:
VB Type | C++ Type |
Byte | unsigned char |
Integer | short |
Long | long |
Single | float |
Double | double |
Currency | __int64 |
When interfacing C++ and Visual Basic, most simple data types are very easy to pass as parameters because they are represented in similar ways. However, more advanced data types such as arrays and objects are more complicated.
Passing By Reference
Passing variables by reference is useful if a function must return more than one value. This process is actually very simple to do; it is the same way in C++ as it would be for local functions. The following is a simple example of this in both C++ and Visual Basic:
Code: Select all
// C++ Code:
void __declspec(dllexport) CALLBACK RetIntByRef(short a,short *t)
{
*t = a * testvar;
}
Code: Select all
Declare Sub RetIntByRef Lib "../testdll_library.dll" (ByVal a As Integer, ByRef t As Integer)
Structures and UDTs are almost as easy as passing by reference. However, there are a few conventions to keep in mind when using them. One important warning is that they have the potential of becoming very complicated when strings and arrays are included in them. Also, remember that memory can be saved if Integers and Bytes are defined next to each other.
The only rule for structures is that they must be passed by reference. Since Visual Basic passes by reference by default, only the C++ DLL must account for this.
Arrays
In C++, raw arrays are very dangerous and can easily cause terrible problems, especially when it receives data from a Visual Basic application. The reason for this is that Visual Basic arrays are much safer and the application will assume it has this “safety net” for arrays, even when they are passed to a C++ DLL. Luckily, Visual Basic stores arrays in the same way as the SAFEARRAY structure in C++.
To accept an array argument from a Visual Basic application, it must be declared as follows:
Code: Select all
short __declspec(dllexport) CALLBACK ArrayExample(LPSAFEARRAY FAR *ArrayData)
{
short *temp;
temp = (short*)(*ArrayData)->pvData;
return(temp[0]);
}
If any data is modified in the newly created array, it is also modified within the Visual Basic application. This is because temp is just a pointer to the passed array’s location in memory.
Important note: When using arrays, remember that indexing is different in both languages. The array index begins at zero in C++ applications and (by default) at one in Visual Basic. This is very important when an application passes an array index to a C++ DLL.
Strings
In Visual Basic, strings are stored in the same way as the C++ BSTR type. However, when an application passes a string by value, it actually passes a pointer to the beginning of the string data. This makes it very easy to work with in C++ because the additional header information is not included. There is, however, a small amount of overhead required to work with strings:
Code: Select all
BSTR __declspec(dllexport) CALLBACK StringExample(BSTR stringVar)
{
LPSTR buffer;
buffer = (LPSTR)stringVar;
::MessageBox(NULL,buffer,"in C++",0);
buffer = _strrev(buffer);
return(SysAllocString(stringVar));
}
There are many system functions that aid in the use of BSTRs. It is strongly recommended that the reader utilize these functions in his or her use of strings. In addition, the example code includes a header file (DLLutil.h) that has several functions to aid in the use of BSTRs.
Returning Strings
Unlike most data types, strings must be handled in a special manner in C++, especially when they are returned from a function. In order to return a string, a system function must be called to properly allocate memory for it. Otherwise, the string will disappear when the function terminates and the client will receive garbage. The following is a simple example of returning a string:
Code: Select all
return(SysAllocString(tempArray[index]));
Callback Functions
There are many situations in which a C++ DLL must call a function in the client application. In such instances, the DLL must be given a list of the application’s function pointers. This is done through the use of the AddressOf operator, which is only available in Visual Basic 5.0 or later.
Setting up callback functions requires a great deal of overhead from both Visual Basic and C++.
The Client Application
Before a DLL can access callback functions, the client application must give it the addresses of each function to be made available to it. This process requires a separate code module in the client application. Within the module, a procedure should be placed to pass the actual information. Next, the prototype for the receiving function must be declared. Any address in Visual Basic is stored as type long; therefore, the function should be declared in a manner similar to the following:
Code: Select all
Declare Sub CallbackExample Lib "../testdll_library.dll" (ByVal Addr As Long)
The DLL
Handling callback functions in C++ is not necessarily difficult, but does require a great deal of work. First, a function must be declared that can receive the callback functions’ addresses in memory. Next, the appropriate declarations must be made to tell C++ how to call the function. The final stage is to assign the address of the function to a pointer, then it can be called as though it were a normal, C++ function. The following is a simple example of this process:
// In C++
Code: Select all
void __declspec(dllexport) CALLBACK CallbackExample(long Addr1)
{
typedef void (__stdcall *FNPTR)(BSTR stringVar);
FNPTR FunctionCall;
LPSTR buffer = "hello!";
FunctionCall = (FNPTR)Addr1;
BSTR temp;
temp = ChartoBSTR("hello");
FunctionCall(SysAllocString(temp));
}
Code: Select all
Public Sub voidFunc(ByVal stringVar As String)
MsgBox stringVar
End Sub
Miscellaneous Functions and Notes
Windows provides several functions with which to enhance a DLL. Two of these functions are GetModuleFileName and GetProcAddress. In order to find the full path of the client application, GetModuleFileName must be called in the following manner:
Code: Select all
char module[50];
GetModuleFileName(NULL,(LPTSTR)module,50);
Code: Select all
TestSub = (cfunc)GetProcAddress((HMODULE)hLib,"TestSub");
Another important aspect of developing DLLs is to make sure to make its use as easy as possible for the client. In order to do this, the developer should provide both a C++ header and a Visual Basic module, which declares everything the client needs to use the DLL file. In the case of C++ clients, the header file should also include a special procedure to setup all the available DLL functions. If callbacks are used, the Visual Basic application should also include such a procedure. It is also recommended that the Visual Basic typedefs are utilized to ensure that the proper data types are used.