DLLs
DLL Overview
There have been many articles written about Dynamic Link Libraries, and
just as many programming books have devoted at least a chapter or two to
this topic. Several of these sources are listed in the Reference section
or this book. This chapter concentrates on several examples of how DLLs
can be used, what to look for in selecting a particular function for a
DLL inclusion, and what o avoid putting in a DLL at all costs.
As the name Dynamic Link Libraries suggests, these libraries are not
linked into the .EXE file during the .EXE creation, rather they get loaded
dynamically into the system memory at runtime. The overwhelming advantage
of DLLs is their ability to save system resources. Once the DLL is loaded,
its functions are available immediately for use to all of the system's
processes. On the other hand, DLLs require complex object linking and process
loading tool implementation. Overall, however, DLLs live up to their claim
to fame - they save system resources and offer much more rapid successive
loading of executable modules that share the common functions than do statically
linked .EXEs.
Another subtle advantage of DLLs is the ability of the programmer to
control the functionality available to the user. For example, a programmer
writing a terminal emulation application could implement a basic set of
functions and label that the base package. Later, if the user demanded
more functionality, the additional features could be compiled and linked
into a series of DLLs that would be available to the user at an additional
cost. This way users could purchase only the functionality required, nothing
less, nothing more. This particular approach yields itself very nicely
to a DLL implementation. One of the DLLs, for example, may contain the
Zmodem protocol while another contains a 3270 terminal emulation filter.
So far, the discussion has centered around generic DLL functionality.
Windows 3.x, OS/2, NT, and Windows 95 have implemented DLL support, but
the way DLLs are loaded, unloaded, initialized, and terminated differs
with each operating system. Since this hook concerns itself with OS/2,
the 0S/2 specifics are of the most interest here. One of the peculiar OS/2
implementations is the way DLLs are loaded into memory. Theoretically OS/2
has a 4 gigabyte memory limit; practically, however, the user only has
512 MB of real memory available to applications. The limit is artificially
imposed by the OS/2 process loading mechanism, which is related to
the OS/2 l.x compatibility issues. In particular, the LDT tiling (this
is discussed by Michael Kogan, 1990) limits the 32-bit OS/2 process address
space to 512 MB. The system loader will attempt to use the upper memory
area for any shared code, which includes DLLs that allow shared data, while
the DLLs and EXEs with nonshared data will be loaded in the lower memory
area. Figure 6.1 depicts this process.
Figure 6.1 System memory map.
512 MB
|
Shared DLLs and .EXEs |
Unallocated
|
Nonshared DLLs and .EXEs |
|
|
0 MB |
Thunking
The compatibility issues between the 32-bit and the 16-bit OS/2 modules
demand a particular transition implementation called thunking. DLLs are
greatly affected by this thunking mechanism. Both the 16-bit .EXE to 32-bit
DLL transition, and the 32-bit .EXE to 16-bit DLL transition must be considered.
The following examples explain why this is necessary.
In the 16-bit to 32-bit case, the 16-bit .EXE file may have been implemented
in such a way that converting to 32-bit is tedious and unnecessary, resulting
in poor performance benefits and other insignificant improvements. On the
other hand, some DLLs that perform 16-bit drawing routines, for example,
may benefit greatly from being converted to 32-bit modules. Also, large
data structures that span 64K require careful manipulation under the 16-bit
implementation; in 32-bit mode the implementation is greatly simplified.
In such cases, a developer may choose to convert the performance-sensitive
sections-DLLs - of the applications to the 32-bit model, while leaving
the base core as a 16-bit .EXE. The opposite transition of 32-bit to 16-bit
may be required because some support libraries that the application
uses are purchased 16-bit .OBJs or DLLs, and while the vendor may or may
not provide the equivalent 32-bit versions of these tools, the application
need not suffer a schedule slip. A 32-bit EXE access to a 16-bit DLL can
be allowed easily.
DLL Performance
Although DLLs are designed to improve system resource usage, a few performance
implications as they relate to DLL management must be understood. There
are really two distinct ways to use the functions that comprise a DLL.
The first and most automatic method is to create an import library, and
it to resolve any references to the functions that are located inside the
DLL The system will automatically load and link the DLL functions at runtime.
One thing to remember, however, is that every time a DLL function call
is made, an associated address fixup must be resolved. These fixups may
present somewhat of a performance impact if the memory that contains the
fixup tables happens to be swapped out to disk while the call to a DLL
function is made. Before an address fixup can be resolved, the tables have
to be brought back; in a resource-constrained system, this can amount to
a considerable performance hit.
In order to avoid a problem with fixups Dynamic link libraries, David
Reich's technique of DLL aliasing can be used. Outlined in his book Designing
OS/2 Applications, he suggests the creation of an alias
function with the same parameters as the DLL function that will be called.
Then you just turn around and call the corresponding DLL function with
the same parameters as the aliased one. By doing this, you are guaranteed
to have only one fixup per each function in your DLL. Of course, this technique
is helpful only when a particular DLL function is called numerous times
throughout the .EXE. Having only a few references to a DLL function does
not warrant the creation of an alias.
Portability is another good reason for aliasing some of the functions.
Imagine if a developer wanted to migrate an application from one operating
system to another. Sometimes using operating system-specific APIs cannot
be avoided, but by aliasing some of these the migration path is much easier.
The programmer is left with porting a single API reference as opposed to
numerous references throughout the code.
Simple DLL Example (32-32)
In order to preserve legacy applications' environments, the current version
of OS/2 for the Intel platform allows applications to mix memory models
when it comes to 16-bit and 32-bit code. It is perfectly acceptable to
have a 32-bit executable call a 16-bit DLL, which in turn can call another
16-bit or 32-bit DLL. A 16-bit executable also can call a 32-bit DLL, and
so forth. The only problem that may arise in doing this is memory model
compatibilities. Compatibility is just a general description of pointer
conversion. Both the DLL and the .EXE must know that pointer conversion
must occur and take careful precautions to avoid a conversion error.
Most bugs with mixed mode 16-bit/32-bit function calling are found in pointer
arithmetic code. The compiler does a great job of helping the programmer
convert the pointers correctly, as the following examples show. For
a detailed compiler description of this thunking conversion technique,
see the IBM C Set/2 User's Guide or IBM C/C++ FirstStep Tools: Programming
Guide.
The most straightforward example of DLL creation and usage employs a
32-bit executable calling a 32-bit DLL. In this case, there are no memory
model mixing considerations, and the programmer can freely pass values
and pointers to any of the DLL functions without regard to conversion problems
that are usually associated with the mixed memory environments.
The main section of the program does little more than call an externally
declared function called MyDLLFunction, which requires
two parameters. One parameter is a pointer to a function, and the other
is a character pointer. Once inside the DLL, MyDLLFunction uses
the input function pointer and passes the character pointer to that function.
The user never knows how this function is implemented as it is hidden inside
the DLL. At the same time, passing a function pointer to the DLL allows
the DLL to call back to the .EXE if the function pointer happens to point
to the function in the calling .EXE module. This, for example, may allow
the DLL to "signal" the .EXE when the DLL is done with a particular task
but has not completed the rest of the work yet. SIMPLE.C provides the first
32-bit to 32-bit .EXE to DLL example.
SIMPLE.C
SIMPLE.MAK
SIMPLE.DEF
MYDLL.C
MYDLL.MAK
MYDLL.DEF
Creating the .EXE and the DLL
A couple of things need to be said about how this .EXE and DLL are built.
First, the .EXE is compiled the same way .EXEs always are compiled. There
are no special considerations. There are, however, two ways to link the
.OBJs to create an .EXE that uses DLLs.
The first method employs an IMPORTS statement in the .EXE DEF file
and specifies the exact DLL name and the exported function names. The second
one relies on a DLL import library that is linked in just as a static library
would be. Using the import library is more of an automatic linking process,
because you do not have to keep track of all of the functions called in
the .EXE. From a maintenance standpoint, the import library is the preferred
linking choice. The import library is created by running the IMPLIB.EXE,
an OS/2 Toolkit utility, and specifying the DLL DEF file or the DLL itself
as a parameter. The import library allows the linker to resolve all of
the references to the DLL resident functions. Note that the import library
or the DEF file with the IMPORTS keyword and functions defined is required
only when the DLL resident functions are invoked automatically by the .EXE.
0S/2 provides another method of loading the DLLs at runtime and calling
the DLL resident functions explicitly. In this fashion, neither the imports
library nor the IMPORTS keyword and functions specification is needed in
the DEF file. An example of this loading technique is covered later in
this chapter.
The DLL can be considered just as a special .EXE file, and in the earlier
releases of some of the operating systems DLLs actually had .EXE extensions.
The main difference is that a DLL cannot execute without a parent .EXE.
In comparison with the .EXE creation, the DLL files must be compiled with
a DLL flag ON (for C-Set: /Ge-). This may not be a requirement for other
compilers. Next the DLL object code must be LINKed and the DLL created.
The most important file for the LINK step (and again, this is for IBM C-Set/2
C/C++) is the proper use of the module definition file (DEF). The DEF'
file specifies how the DLL will be loaded, named, shared, and so forth.
LINK386.EXE, a 32-bit linker for OS/2, recognizes module definition keywords
listed in Table 6.1.
Table 6.1. Module Definition Keywords
Keyword |
Description |
BASE |
Preferred load address |
CODE |
Code segments attributes |
DATA |
Data segments attributes |
DESCRIPTION |
Module description |
EXETYPE |
DLL operating system type |
EXPORTS |
Functions exported by DLL |
IMPORTS |
Functions imported by EXE/DLL |
HEAPSIZE |
Local heap size |
LIBRARY |
DLL name |
NAME |
EXE name |
OLD |
Preserve old ordinal numbers |
PHYSICAL DEVICE |
Device driver name |
PROTMODE |
Protected mode only module |
SEGMENTS |
Segments attributes |
STACKSIZE |
Local stack size |
STUB |
Pretended DOS executable module |
VIRTUAL DEVICE |
Virtual device driver name |
The definition module must specify the correct combination of keywords
so that the linker can construct the DLL or .EXE file correctly.
Detailed explanation of the linker recognized keywords can be found
in the online OS/2 Toolkit documentation (OS/2 Tools Reference: TOOLINFO.INF).
|
Gotcha!
IMPORTS 1mydll.MyFunction1 statement fails due so a parser. The parser
of IMPORTS does not expect a number as the first character of a DLL
even though the DLL name is a legal OS/2 file name.
|
16-32, 32-16 Transitions
OS/2 supports four classes of applications:
-
Pure 16-bit
-
Mixed 16-bit
-
Pure 32-bit
-
Mixed 32-bit
The pure 16-bit application development was left behind in OS/2 l.x days,
and the pure 32-bit application development with DLLs is covered in the
SIMPLE.DLL example. This leaves only two interesting cases:
-
16-bit .EXE calling 32-bit DLL
-
32-bit .EXE calling 16-bit DLL
The most interesting item in mixed programming is the transition from one
memory model to the other and back. This transition in OS/2 is achieved
with the help of a mapping layer technique called thunking. A 32-16 thunk
and a 16-32 thunk are possible. Thunking involves converting 32-bit pointers
to 16-bit pointers, and vice versa. This thunking mechanism is a requirement
for all mixed mode applications. Luckily for the programmer, the compiler
generally supports the thunking transitions automatically.
The 16-bit memory model has 64K segmentation size limitations, while
the 32-bit memory model does not. Therefore, if a 16-bit .EXE needed to
manipulate a large data area (>64K), rewriting just the manipulation
routines and composing them into a 32-bit DLL would work.
Call a 32-Bit DLL from a 16-Bit Program
The 16-bit to 32-bit example is a simple checksum program that operates
on a data area greater than 64K in size. Both the DLL and the .EXE source
code are rather simple. The interesting part is the way the functions are
declared in the 16-bit source and in the 32-bit source. The sires of the
arguments must match across the transition boundary. In this case, all
of the parameters and the return value are of the same size in the 16-bit
and the 32-bit sections of the code.
The 16-bit executable makes a call to the 32-bit DLL requesting the
checksum value by passing a file name to the 32-bit DLL function. The 32-bit
DLL is invoked automatically by the system. The DLL function proceeds to
use the 32-bit APIs to determine the file size (DosQueryPathInfo
),
allocate the memory (malloc > 64K), open the file (DosOpen
),
and read the data (DosRead ). The checksum calculation is
made nest, and the values are returned to the caller.
HOWBIG.C
HOWBIG.MAK
HOWBIG.DEF
COUNT.C
COUNT.MAK
COUNT.DEF
Pointer Declarations
When passing a pointer to a 16-bit function from a 32-bit program, the
_Seg16 type qualifier should be used. For example:
char *_Seg16 ptrForl6Bit ;
declares this pointer to be a segmented pointer that is usable in 16-bit
functions. It is also usable in a 32-bit program.
Calling a 16-Bit DLL from a 32-Bit Program
A similar transition takes place when calling the 16-bit DLL from a 32-bit
.EXE. The function declarations utilize the same keywords that were used
in the 16-bit to 32-bit example earlier. This particular program attempts
to determine whether the computer's serial ports utilize the faster buffered
I/O National 16550 UARTs (Universal Asynchronous Receiver/Transmitter).
In order to do this, the program employs a 16-bit I/O DLL. called l6BITIO.DLL.
This DLL. contains two functions, my_inp and my_outp
.
These functions will directly input or output a single byte from or to
the specified I/O port. A 16-bit DLL is used to demonstrate how quickly
the presence of the National 16550 UART can be determined. The algorithm
for determining the presence of the UART is trivial and described in the
National UART Devices Data Book.
|
Gotcha!
In order to perform direct h/w I/O the code must run at the RING
2 Input/Output Privilege Level (IOPL). This is why the appropriate
CODE statement is found in the DEF file for the l6BITIO.DLL. Unfortunately,
there is no IOPL support for the 32-bit DLLs; thus 16-bit IOPL DLLs must
be used in such cases. This may change in future
releases, but for now we are limited to using 16-bit code.
The above statement was true till 1998 when
Rinat Sadretdinow invented iopl32.dll (901 bytes long !!!)
|
AUT16550.C
AUT16550.H
AUT16550.MAK
AUT16550.DEF
16BITIO.C
16BITIO.MAK
16BITIO.DEF
Loading/Unloading of DLLs
As was mentioned earlier, developers have two choices about loading and
unloading the DLLs. They may choose to have the system do the work for
them automatically, or they may decide to have complete control over how
DLL functions are loaded, unloaded, and called.
The automatic loading and unloading of DLLs is the most headache-free,
low-maintenance option. Bus it does have some drawbacks. The application
cannot be started without the DLL being present in the LIBPATH. Nor can
the resources used by the DLL be freed up until the application exits.
If resource considerations are of great importance, the manual method of
loading and unloading DLLs must be used. The benefits of manual manipulation
of DLL functions are obvious: low memory usage, no initialization of DLLs
at application startup time, resources can be freed when not needed, application
can recover if DLL is missing or corrupted, and so on. The drawback to
using the manual option is complexity.
The previous example of 32-bit to 16-bit CHK16550.EXE is used here to
illustrate the manual loading, usage, and unloading of a DLL. First a call
to the DosLoadModule is made.
APIRET DosLoadModule (PSZ pszName, ULONG cbName,
PSZ pszModeleName, PHMODULE phMod )
pszName is the address of buffer used in case of failure; on
output it will contain the name of the object that caused the failure.
cbName
is the size of the pszName buffer. pszModuleName is the name
of the dynamic link library, and phMod is a pointer that on output
contains the handle for the dynamic link module.
Next, the starting address of a function is found using the DosQueryProcAddr
.
APIRET DosQueryProcAddr( HMODULE hmod,
ULONG ulOrd, PSZ pszName, PFN *ppfn)
hmod is the dynamic link module handle. ulOrd is the ordinal
number of the function whose address is to be found. If this value is 0,
the pszName argument is used to find the desired function. pszName
contains the function name that is being referenced. ppfn is a pointer
to a PFN that on output contains the procedure address.
Once the addresses of my_inp and my_outp
are known, the program run's the same way. Last, the DosFreeModule
is called to release the DLL and effectively unload it from CHKI6500.EXE's
memory space.
APIRET DosFreeModule (HMODULE hmod
)
This function has only one parameter, hmod, which is the handle
of the module that is to be freed.
MAN16550.C
MAN16550.H
MAN16550.MAK
MAN16550.DEF
|
Gotcha!
If using a DosExitList in a DLL, the DLL cannot be freed Via DosFreeModule
until the exit list function has run.
|
Optimizing Performance in DLLs
System performance can be improved significantly by efficient use of DLLs.
These performance improvements can be gained from something as simple as
combining several smaller DLLs into one larger one, or by using David Reich's
'aliasing' technique in helping the fix-up problems. The following checklist
lists some good DLL candidates.
-
Rarely called functions
-
Functions that add functionality to the base product
-
Functions that remove functionality from the base product
-
Functions that can be shared among applications
-
Functions with frequently changing internal implementation
-
Internationalization enabling functions
-
Help and Message type functions