Paul Giglio is a contract developer for Dowell Schlumberger in Tulsa, OK. David Schwartz is a developer for Dimensional Concepts in Stillwater, OK. They both hold a B.S. in Computer Science from Oklahoma State University and are continually looking for faster, smaller, better. Paul may be reached via e-mail at giglio@tulsa.dowell.slb.com.
Introduction
Dynamic linking is a mechanism provided by the Windows API that allows executable code to be segmented into distinct modules called dynamically linked libraries (DLLs). An application can, with some restrictions, directly call the functions provided by a DLL after linking with it. Furthermore, any given DLL can be concurrently linked to and used by multiple applications. This article will focus on reasons for using dynamic linking, and explicit dynamic linking in particular. It will also present a method for implementing explicit dynamic linking superior to typical textbook examples.There are several advantages to using dynamic linking. Moving code common to several applications into a single DLL eliminates redundant code and results in a more modular system. Encapsulating rarely changed code into a DLL reduces the amount of code linked to form the application executable and results in faster builds and smaller updates. Also, to some extent, application code can be developed independently from library code.
Dynamic linking can be done either implicitly or explicitly. The major difference between the two is whether links between calls in the application and functions in the DLL are resolved at link time or at run time. The Windows loader automatically loads an application's implicitly linked libraries when the application is started. It automatically unloads them when the application is terminated. Alternatively, the programmer has control over when explicitly linked libraries are loaded and unloaded. For this reason, explicit dynamic linking is more flexible and offers more benefits. However, it is somewhat less straightforward to implement.
Explicit linking thus has two clear advantages:
1) Libraries can be loaded and unloaded as appropriate. Code that is executed only at the end of the year or once a week does not have to be in memory all the time. The initial program load time is reduced, and the application's memory requirements are minimized.
2) An application may replace one library with another at run time. Different back-end code for systems such as text converters or database wrappers can be placed in separate DLLs, so that only the code needed for the task at hand needs to be in memory (for example, Word's WordPerfect converter, or ODBC's Paradox translator).
Implementation
The application side of the code must get the address of each and every DLL function it intends to use with a GetProcAddress call, then store that address in a function pointer. Typically, the program defines global function pointers and includes a block of code to do the GetProcAddress calls. An application that uses 50 or more DLL functions is not uncommon. Maintenance of this bloated code quickly becomes tedious.The library side of the code must export details about each function it intends to make available to the application. The source code for an exported function differs from a non-exported function only in that an extra modifier is added to the function definition, so the expense of exporting a function is not immediately apparent. However, when exported functions are compiled, extra instructions are added to handle changing the data segment, and the function's name is added to the DLL's name table where GetProcAddress calls can find it.
The alternative to having a mountain of function pointers and GetProcAddress calls is to implement a single entry point into the DLL. The application side of the code is reduced to one function pointer and one GetProcAddress call. The library side of the code needs to export only the entry point function. The application calls functions in the DLL indirectly via the entry point function by passing it the arguments intended for the DLL function along with an indicator that specifies which DLL function is to be called. The entry point then calls the requested DLL function with the arguments it was passed, and passes the return value back to the application.
Typically, single-entry-point functions are implemented with a large, somewhat convoluted switch statement. There are disadvantages to this method. The switch is difficult to maintain and must be evaluated for each function call. Also, the arguments to each function must be re-pushed onto the stack by the entry-point function. The solution presented below solves these problems. It offers reduced code size and execution time, is easy to follow, and is arguably easier to maintain.
The Application Code
Calls to the entry point function are encapsulated with an enumeration (Listing 1) and a set of inline functions (Listing 5) . The enumeration supplies a set of unique identifiers corresponding to the DLL functions. An inline function is defined corresponding to each DLL function. Each inline function takes the same number and types of arguments and returns the same type as its counterpart in the DLL.At compile time, the inline functions are expanded into calls to the entry point function with the appropriate enumeration value added to the argument list. In this way, calls to DLL functions throughout the application code are identical to calls made to local functions. The details of calling into a DLL are effectively hidden (Listing 5) .
The Library Code
Calls to the single-entry-point function are to be handled with a minimum amount of overhead. An array of pointers to the DLL functions is defined that corresponds to the enumeration in the application code. When the entry point is called, a small amount of inline assembly language code provides the DLL function to be called with direct access to its arguments (Listing 2) . The inline assembly language code pops everything off the stack down to the argument list. The last argument pushed onto the stack by the calling function, the function identifier, is then popped as well.The result is that the DLL function's arguments are in place on the stack. Without making any further stack modification, the entry point passes control directly to the DLL function. When control returns to the entry point, additional inline assembly language code restores the stack, and control is returned to the application. Because the entry point function returns type void, any value returned by the DLL function is unaffected and is available to the application.
Keeping the enumerated function identifiers parallel to the function pointer array in the DLL is vital, but can be difficult. One approach is to use the preprocessor to build the enumeration in the application's code and the array of function pointers in the DLL's code from a single header file. This technique is shown in the shared header file, Listing 1. Listing 2 is library source code, and Listing 3, Listing 4, and Listing 5 are the application header, resource, and source code respectively. A Borland-compatible makefile is shown in Listing 6.
Special Considerations
When implementing multiple back ends in separate libraries, the same header file should be used by each of the libraries to declare the functions called by the front end. This practice will ensure a consistent interface between the front end and each of the back ends by causing compiler errors to be generated if any of the back end functions are missing or improperly defined.The inline assembly language code used in the DLL entry point is designed to work with the 16-bit Windows code produced by the Borland C++ 3.1 compiler. However, assembly language code to handle 32-bit Windows calls or any changes introduced by using another compiler can also be easily implemented. Compile the entry point function with the "generate assembly" switch enabled and examine the stack at the entry point. Everything pushed onto the stack by the entry point, the return address, and the function ID must be popped and saved before calling the DLL function, then restored after it returns. Note that because the entry point function takes no arguments, code to generate the stack frame (push and pop bp) may not be produced by some optimizing compilers.
A few other compiler dependencies should also be addressed. float may or may not be a valid return type for a DLL function. The type of the function ID in the entry point may need to be adjusted so that it is the same size used for enumerations by your compiler (i.e. make func_id a char if enumerations use one byte). Popping the wrong number of bytes for the function ID would be disastrous.
Conclusion
Explicit dynamic linking is not the solution to every problem, but when applicable it offers greater flexibility and performance than a single module or implicitly linked DLLs. The method presented here allows these benefits to be realized at a much lower cost than the traditional method.