Copyright Notice: This article is copyright 2002 by Nicholas
Skapura. This article is reproduced here with his
permission. This article originally appeared on flipcode.com
which is now closed down.
Introduction
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:
1) Open Visual Basic and select new ActiveX DLL.
2) Rename the Class and Project to reflect the name that the client application will use in its code.
3) Add any member functions as you would to a normal class module.
The DLL is now ready to compile, however, it is usually a good idea to
create a test application for debugging purposes. This is done by
clicking File -> Add Project… -> Standard EXE.
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:
- ' 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
The Client Application
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:
- Set Library = New testDLL
When the class instance is created, its initialization function is also called. This is demonstrated in the example code.
Calling member functions is exactly the same as a standard class module:
- Call Library.Basic_Sub_Call
When the instance of the class is destroyed, its terminate function is
called so the DLL can clean up any memory it has allocated.
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:
1) Open Visual C++ and select New Win-32 Dynamic Link Library.
2) Select the radio button labeled A Simple DLL Project.
3) Click Project -> Add to Project -> New… -> Text File
4) Name the file the same as your module with the .DEF extension (e.g. testDLL.def).
Setting up for Visual Basic
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:
- void __declspec(dllexport) CALLBACK TestFunc()
- {
- cout << "Inside the DLL!";
- }
After all the necessary functions are created, they must be included in
the .DEF file so Visual Basic knows what to look for. Follow these
steps to accomplish this:
- LIBRARY "testDLL_Library"
- DESCRIPTION "An example DLL for interfacing with C++"
-
- EXPORTS
- TestFunc @1
- RetInt @2
- OtherFunc @3
One very important requirement for Visual Basic is that the LIBRARY
keyword must be the same as the final DLL filename. Otherwise, Visual
Basic will not understand what it is looking at and produce an error.
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:
- typedef void (WINAPI*cfunc)();
- typedef int (WINAPI*ifunc)(int t);
The name of the typedef statement appears after the WINAPI keyword (e.g. cfunc and ifunc).
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:
- cfunc TestFunc;
- ifunc RetInt;
The next step is to load the DLL into memory and obtain an instance of it. This is accomplished as follows:
- HINSTANCE hLib = LoadLibrary("testdll_library.dll");
- if(hLib == NULL)
- {
- cout << "ERROR: Unable to load library!" << endl;
- getch();
- return;
- }
It is very important that error checking is included here. Otherwise,
the client application will crash when it attempts to call a function
from the DLL.
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:
- TestFunc = (cfunc)GetProcAddress((HMODULE)hLib,"TestFunc");
- RetInt = (ifunc)GetProcAddress((HMODULE)hLib,"RetInt");
Now, the functions can be called as though they were declared within the client application.
Before the client application terminates, it is important to unload the
DLL from memory. This is accomplished by calling FreeLibrary:
- FreeLibrary((HMODULE)hLib);
The Client Application (Visual Basic)
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:
- Declare Sub TestFunc Lib "../testdll_library.dll" ()
- Declare Function RetInt Lib "../testdll_library.dll" (ByVal t As Integer) As Integer
One important note is that all parameters must be passed with the ByVal
keyword. This is because Visual Basic always passes parameters ByRef by
default. Since C/C++ usually passes by value, this must be specified in
the Visual Basic declaration. This is all that is necessary when
standard C variable types (int, long, etc.) are used, however complex
data types like strings and arrays require more overhead on both the
client and DLL sides.
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 |
Advanced Data Types
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:
- // C++ Code:void __declspec(dllexport) CALLBACK RetIntByRef(short a,short *t)
- {
- *t = a * testvar;
- }
-
- // Visual Basic Code:
- Declare Sub RetIntByRef Lib "../testdll_library.dll" (ByVal a As Integer, ByRef t As Integer)
Structures and User-Defined Types
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:
- short __declspec(dllexport) CALLBACK ArrayExample(LPSAFEARRAY FAR *ArrayData)
- {
- short *temp;
- temp = (short*)(*ArrayData)->pvData;
- return(temp[0]);
- }
Before any operations are performed on the array, it must be converted
into a standard C-Style array. First, a pointer must be declared of the
same type of data in the array. Next, the address of the SAFEARRAY data
must be copied to the new variable. Notice the (short *) typecast; this
is necessary because Visual C++ will generate an error without it. Once
the array has been converted, it can be modified as a normal C-Style
array would.
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:
- BSTR __declspec(dllexport) CALLBACK StringExample(BSTR stringVar){
- LPSTR buffer;
- buffer = (LPSTR)stringVar;
- ::MessageBox(NULL,buffer,"in C++",0);
- buffer = _strrev(buffer);
- return(SysAllocString(stringVar));
- }
-
Notice the use of typecasting to convert the string. It is possible to
discard the use of the BSTR type altogether, but this is not
recommended. The reason for this is because it is accepted that BSTR is
the data-type used for Visual Basic strings. If changes were made to
how Visual Basic handles strings, it could break the DLL’s code.
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:
- 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:
- Declare Sub CallbackExample Lib "../testdll_library.dll" (ByVal Addr As Long)
The actual callback functions usually should receive their arguments by value, unless the argument’s value will be altered.
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++
- 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));
- }
-
- // Here is the corresponding Visual Basic function:
-
- 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:
- char module[50];GetModuleFileName(NULL,(LPTSTR)module,50);
The
GetProcAddress function is useful for finding the address of functions in another DLL:
- TestSub = (cfunc)GetProcAddress((HMODULE)hLib,"TestSub");
This tutorial also includes a header file with several useful
functions. It includes useful
typedefs to help declare proper data
types across both modules as well as several functions to aid in the
use of
BSTRs. The reader is encouraged to examine this header file and
use the included functions.
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.
Conclusion
Given the information in this tutorial, the reader should now have a
firm understanding of how to link a C++ DLL with a Visual Basic
application. Included with this tutorial (
article_vbdllcode.zip(223KB))
are several examples of each technique discussed. In addition, the zip
file also contains a useful C++ header file with several useful
functions and typedefs to aid in the development of DLLs.
If you have any questions or comments regarding this tutorial, please e-mail me at skapura.2@wright.edu.
http://skap.8k.com