MORE INFORMATION
SECTION A
1.0 What Is a DLL?
DLLs (Dynamic Link Libraries) are an important aspect of Windows. A DLL
contains functions that your executable program can call during execution.
In other words, a DLL is a library of functions that your program can link
with dynamically.
A link can be static or dynamic. Static links don't change. All the address
information needed by your program to access the library function is fixed
when the executable file is created and remains unchanged during execution.
Dynamic links are created as needed. When your program needs a function
that is not in the executable file, Windows loads the dynamic link library
(the DLL), making all of its functions available to your application. At
that time, Windows resolves the address of each function and dynamically
links it to your application.
All Custom controls used in Visual Basic are DLLs. The only difference is
that they require special handling in terms of messages received from
Visual Basic.
1.1 Why Use DLLs?
Here are four reasons why you might want to use a DLL:
- Access to C Run-Time Functions:
The C run-time library has many useful functions that would not be
available to Visual Basic programmers were it not for DLLs. For example,
the _dos_getdiskfree function allows you to calculate the total amount
of disk space and the free disk space available on a drive.
- Access to Windows API (Application Programming Interface) Functions that
Require Callback Routines:
Some Windows API functions require a callback function. A callback
function is a function that Windows will call while executing the API
call. An example of this sort of function is EnumTaskWindows, which
will give the handle of all windows that are owned by a particular task.
- Speed:
C is a fully compiled language that works at a level that is fairly
close to native machine code. This means that the execution of programs
that are well written in C will be fast.
- Load on Use:
Code and data from a DLL are loaded only when needed. A DLL can be
organized such that only required parts are loaded as opposed to the
entire DLL. This reduces the amount of memory required and the time
taken to load.
1.2 Anatomy of a DLL
Every DLL must contain a LibMain function and should contain a Windows Exit
Procedure (WEP) in addition to the exported functions that can be called by
an executable program.
- LibMain:
A DLL must contain the LibMain function. The LibMain function is called
by the system to initialize the DLL. LibMain is called only once -- when
the first program that requires the DLL is loaded. The following are the
parameters passed to LibMain:
- HANDLE : Handle to the instance of the DLL.
- WORD : Library's data segment.
- WORD : Heap size.
- LPSTR : Command line parameters.
- WEP:
The WEP (Windows Exit Procedure) performs cleanup for a DLL before the
library is unloaded. Although a WEP function was required for every DLL
in previous versions of the Windows operating system, for version 3.1 it
is optional. A WEP should be included in the module definition file
(.DEF) in Visual C, for example:
- Exported Functions:
These are the functions you want to call from your DLL. They are denoted
by _export. _export is used for backward compatibility. All the
functions you want to call must also be listed in the (.DEF) file of
your DLL.
1.3 DLL Memory Management Issues
Use the large memory model.
C stores all variables defined as static or global (defined outside of a
function) in the program's heap space, and C stores all other variables on
the stack.
In the small and medium model, all pointers are near by default. This means
that the data is accessed by 16-bit offsets to either the data segment (DS)
register, or the stack segment (SS) register. Unfortunately, the compiler
has no way of knowing whether the offset is from the DS or the SS. In most
programs this would not be a problem because the DS and SS point to the
same segment. A DLL, however, is a special case.
A DLL has its own data segment but shares its stack with the calling
program. This means that the DS and the SS do not point to the same
location. The easiest solution to this problem is to build the DLL in the
large memory model where all variables are referenced by a 32-bit value.
Why Allocate Memory Dynamically?
Allocating memory dynamically is a Windows-friendly technique. Declaring
large arrays of data takes up space in either your program's stack, which
is limited to 64K, or you program's Data Segment, which wastes disk space
and Windows memory. It is better to ask Windows for the memory when you
need it, and then free it when you have finished.
Allocating Memory
In Windows, you can dynamically allocate two types of memory, local and
global. Local memory is limited to 64K, and in the case of a DLL, local
memory is shared with the program that called the DLL. Global memory is
all of the memory available to Windows after it has loaded.
Local memory is allocated and managed using the LocalAlloc, LocalLock
LocalUnlock, and LocalFree functions -- as in this example:
char* pszBuffer;
....
pszBuffer = (char *) LocalAlloc (LPTR, 20);
...
LocalFree (pszBuffer);
It is faster to allocate local memory than it is to allocate global memory.
But allocations from the local heap are limited to 64K, which must be
shared amongst all programs that are calling the DLL. It is best to use
local memory when small, short lived blocks of memory are required.
Global memory is allocated and managed using the GlobalAlloc, GlobalLock
GlobalUnlock, and GlobalFree functions -- as in this example:
HGLOBAL hglb;
char* pszBuffer;
hglb = GlobalAlloc (GHND, 2048);
// GHND allocates the memory as moveable and
// initialized to 0
// 2048 is the amount of memory to be allocated...
pszBuffer = GlobalLock (hglb);
...
GlobalUnlock (hglb);
GlobalFree (hglb);
The GlobalAlloc function allocates memory in multiples of 4K.
If you want to share memory allocated in the DLL with other programs, you
should allocate it using the GMEM_SHARED flag. If you want to share the
memory through DDE, you must allocate it by using the GMEM_DDESHARE flag.
Be Careful When Storing Data in Static Variables
If you try to store data in a DLL using global or static variables, don't
be surprised if these values have changed when you next call your DLL. The
data stored in this way will be common to all applications that access this
DLL. No matter how many applications use a DLL, there is only one instance
of the DLL. The best way to get around this is to return structures from
the DLL and pass them in again when they are needed.
File Handles
It is not possible to share file handles between applications or DLLs. Each
application has its own file-handle table. For two applications to use the
same file using a DLL, they must both open the file individually.
1.4 Building a DLL Using Visual C++
Here are the steps necessary to build a DLL using Visual C++:
- Start Visual C++.
- Create a new project by choosing New from the Project menu. Select the
following options:
- Set the Project Type to "Windows dynamic-link library (.DLL)"
- Clear the "Use Microsoft Foundation Classes" check box.
You can also set or view these options later by choosing Project from
the Options menu.
- Add your existing .C and .DEF files to the project by using the dialog
box that comes up when you choose Edit from the Project menu. Or enter
your code directly in the Visual C++ editing window. (See the .C and
.DEF example code listed below.)
- From the Project menu, choose the Build <yourname>.DLL option.
1.5 Example C DLL
The following DLL contains the GetDiskInfo function, which can be called
from Visual Basic. It will return the disk space available, the current
drive name and the volume name.
C Code Example, DISKINFO.C:
#include <windows.h>
#include <dos.h>
int CALLBACK LibMain (HANDLE hInstance, WORD wDataSeg, WORD wHeapSize,
LPSTR lpszCmdLine)
// The following is required only under Windows version 3.1
// Win32 does not require or support UnlockData()
{
if (wHeapSize > 0)
UnlockData (0); //Unlocks the data segment of the library.
return 1;
}
void __export CALLBACK GetDiskInfo (char *cDrive, char *szVolumeName,
unsigned long *ulFreeSpace)
{
unsigned drive;
struct _diskfree_t driveinfo;
struct _find_t c_file;
_dos_getdrive (&drive);
_dos_getdiskfree( drive, &driveinfo );
if (!_dos_findfirst( "*.*", _A_VOLID, &c_file ))
wsprintf( szVolumeName, "%s", c_file.name);
else
wsprintf ( szVolumeName, "NO LABEL");
*cDrive = drive + 'A' -1;
*ulFreeSpace = (unsigned long) driveinfo.avail_clusters * (unsigned
long) driveinfo.sectors_per_cluster * (unsigned long)
driveinfo.bytes_per_sector;
}
Use the following DISKINFO.DEF file in Visual C++:
LIBRARY diskinfo
DESCRIPTION 'GetDiskInfo Can be called from Visual Basic'
EXETYPE WINDOWS 3.1
CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD MOVEABLE SINGLE
HEAPSIZE 4096
EXPORTS
GetDiskInfo @1
NOTE: The LIBRARY name in the .DEF file must be the same as the DLL file
name, or else Visual Basic will give you "Error in loading DLL." For
example, create the file DISKINFO.DLL using the LIBRARY DISKINFO statement
in the .DEF file above.
SECTION B
2.0 Calling DLLs from Visual Basic
In Visual Basic, all functions, including DLL functions, that you want to
call must first be declared by using the Declare statement. You can declare
your functions in the declarations section of a Form or a Module. If you
declare a DLL procedure or function in a Form, it is private to that Form.
To make it public, you must declare it in a Module. The following is an
example Declare statement:
Declare Sub getdiskinfo Lib "c:\somepath\diskinfo.dll"
(ByVal mydrive As String, ByVal myvolume As String, free As Long)
You must enter the entire Declare statement as one, single line. This
particular Declare statement declares the user-defined procedure
GETDISKINFO located in user-created DISKINFO.DLL file.
Once you declare the function, you can call and use the function just as
you would call and use a Visual Basic function.
2.1 DLL Parameters
Because DLLs are typically written in C, DLLs can use a wide variety of
parameters not directly supported by Visual Basic. As a result, when
passing parameters, the programmer has to find the appropriate data type to
pass.
Passing Arguments by Value or by Reference
By default, Visual Basic passes all arguments by reference. (When passing
by reference, Visual Basic supplies a 32-bit far address.) However, many
DLL functions expect an argument to be passed by value. This can be
achieved by placing the ByVal keyword in front of the argument declaration.
The following sections show you how to convert parameters to Visual Basic.
8- to 16-Bit Numeric Parameters
Pass 8- to 16-bit numeric parameters (int, short, unsigned int, unsigned
short, BOOL, and WORD) as Integer.
32-bit Numeric Parameters
Pass 32-bit numeric parameters (long, unsigned long, and DWORD) as LONG.
Object Handles
All handles are unique 16-bit integer values associated with a Window and
are passed by value, so pass these parameters as Integer.
Strings
Strings include the LPSTR and LPBYTE data types (pointer to characters or
pointer to unsigned characters). Pass these parameters as (ByVal paramname
As String). To pass Visual Basic strings directly, pass them as (param As
String).
For additional information on passing strings between Visual Basic and a C
DLL, please see the following article in the Microsoft Knowledge Base:
118643
How to Pass a String or String Arrays Between VB and a C DLL
NOTE: Visual Basic strings require special handling, so don't pass strings
directly unless the DLL explicitly requires it.
Pointers to Numeric Values
Pass pointers to numeric values by simply not using the ByVal keyword.
Structures
If the Visual Basic user-defined type matches the structure expected by the
DLL, the structure can be passed by reference.
NOTE: Structures cannot be passed by value.
Pointers to Arrays
Pass the first element of the array by reference.
Pointers to functions
Visual Basic does not support callback functions, so DLL functions that
have pointers to functions cannot be used with Visual Basic.
Null Pointers
If a DLL expects a Null pointer, pass it as (ByVal paramname As Any). You
can use 0& as the value of paramname when calling the DLL.
2.2 Troubleshooting
Below are solutions to some problems you may encounter.
System Resources Keep Getting Lower After the DLL Is Called
If your DLL is using GDI objects, you must remember to free them after
using them. This may not be obvious in Visual Basic, but when using the
Windows SDK (software development kit) if you create a GDI object (for
example, CreateBrushIndirect), you must delete it by using DeleteObject
later on.
Bad DLL Calling Convention Error
This error is often caused by incorrectly omitting or including the ByVal
keyword from the Declare statement. This error can also be caused if the
wrong parameters are passed.
Error in Loading DLL
This error occurs when you call a dynamic-link library procedure and the
file specified in the procedure's Declare statement cannot be loaded. You
can use the Microsoft Windows API function LoadLibrary to find out more
specific information about why a DLL fails to load.
General Protection (GP) Fault
GP faults occur when your program writes to a block of memory that doesn't
belong to it. The two most likely reasons for this are:
- You overstepped an array boundary. C does not check that the array
subscript you are writing to is valid. Therefore, you can easily write
to memory you don't own.
- You are using a pointer to a memory location that you have freed. The
best option is to assign NULL to all pointers after you free their
memory.
A GP fault can also occur when an incorrect variable type is passed to the
DLL function.
2.3 Example Visual Basic Calling Program
There are two parts to calling a DLL in a Visual Basic program. First you
declare the function, and then you use it in event code.
Here is an example of a Declare statement. The Declare statement should be
put in a module or in a form's General Declarations section.
' Enter the following Declare as one, single line:
Declare Sub getdiskinfo Lib "c:\dllartic\diskinfo.dll"
(ByVal mydrive As String, ByVal myvolume As String, free As Long)
Specify ByVal statements exactly as shown, or else a GP fault may occur.
Once the function is declared, you can use it in event code. The following
example uses a function from the DLL in the Command1 Click event code:
Sub Command1_Click ()
Dim drive As String * 1
Dim volume As String * 20
Dim free As Long
Call getdiskinfo(drive, volume, free)
Text1.Text = drive
Text2.Text = volume
Text3.Text = Str$(free)
End Sub