[Next] [ Previous ]     Chapter 6
   [ Contents ]  [ Chapter 5: Interprocess communication ]  [ Chapter 7:  Exception Handling ]

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: 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: 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.
  1. Rarely called functions
  2. Functions that add functionality to the base product
  3. Functions that remove functionality from the base product
  4. Functions that can be shared among applications
  5. Functions with frequently changing internal implementation
  6. Internationalization enabling functions
  7. Help and Message type functions

[ Next ] [ Previous ] Chapter 6
   [ Contents ]  [ Chapter 5: Interprocess communication ]  [ Chapter 7:  Exception Handling ]