Herb Marselas, Ensemble Studios
D
ynamic Link Libraries (DLLs) are a powerful feature of Microsoft Windows.They have many uses, including sharing executable code and abstracting out device differences. Unfortunately, relying on DLLs can be problematic due to their standalone nature. If an application relies on a DLL that doesn't exist on the user's computer, attempting to run it will result in a "DLL Not Found" message that's not helpful to the average user. If the DLL does exist on the user's computer, there's no way to tell if the DLL is valid (at least as far as the application is concerned) if it's automatically loaded when the application starts up.
Bad DLL versions can easily find their way onto a system as the user installs and uninstalls other programs. Alternatively, there can even be differences in system DLLs among different Windows platforms and service packs. In these cases, the user may either get the cryptic "DynaLink Error!" message if the function being linked to in the DLL doesn't exist, or worse yet, the application will crash. All of these problems with finding and loading the correct DLL are often referred to as "DLL Hell." Fortunately, there are several ways to protect against falling into this particular hell.
Implicit vs. Explicit Linking
The first line of defense in protecting against bad DLLs is to make sure that the nec-essary DLLs exist on the user's computer and are a version with which the application can work. This must be done before attempting to use any of their functionality.
Normally, DLLs are linked to an application by specifying their eponymous lib file in the link line. This is known as implicit DLL loading, or implicit linking. By link-ing to the lib file, the operatlink-ing system will automatically search for and load the matching DLL when a program runs. This method assumes that the DLL exists, that Windows can find it, and that it's a version with which the program can work.
Microsoft Visual C++ also supports three other methods of implicit linking. First, including a DLL's lib file directly into a project is just like adding it on the link line.
Second, if a project includes a subproject that builds a DLL, the DLL's lib file is
33
automatically linked with the project by default. Finally, a lib can be linked to an application using the #pragma comment (lib "libname") directive.
The remedy to this situation of implicit linking and loading is to explicitly load the DLL. This is done by not linking to the DLL's lib file in the link line, and remov-ing any #pragma comment directives that would link to a library. If a subproject in Visual C++ builds a DLL, the link property page of the subproject should be changed by checking the "Doesn't produce .LIB" option. By explicitly loading the DLL, the code can handle each error that could occur, making sure the DLL exists, making sure the functions required are present, and so forth.
LoadLibrary and GetProcAddress
When a DLL is implicitly loaded using a lib file, the functions can be called directly in the application's code, and the OS loader does all the work of loading DLLs and resolving function references. When switching to explicit linking, the functions must instead be called indirectly through a manually resolved function pointer. To do this, the DLL that contains the function must be explicitly loaded using the LoadLibrary function, and then we can retrieve a pointer to the function using GetProcAddress.
HMODULE LoadLibrary(LPCTSTR IpFileName);
FARPROC GetProcAddress(HMODULE hModule, LPCSTR IpProcName);
BOOL FreeLibrary(HMODULE hModule);
LoadLibrary searches for the specified DLL, loads it into the applications process space if it is found, and returns a handle to this new module. GetProcAddress is then used to create a function pointer to each function in the DLL that will be used by the game. When an explicitly loaded DLL is no longer needed, it should be freed using FreeLibrary. After calling FreeLibrary, the module handle is no longer considered valid.
Every LoadLibrary call must be matched with a FreeLibrary call. This is necessary because Windows increments a reference count on each DLL per process when it is loaded either implicitly by the executable or another DLL, or by calling LoadLibrary.
This reference count is decremented by calling FreeLibrary, or unloading the exe-cutable or DLL that loaded this DLL. When the reference count for a given DLL reaches zero, Windows knows it can safely unload the DLL.
Guarding Against DirectX
One of the problems we have often found is that the required versions of DirectX components are not installed, or the install is corrupt in some way. To protect our game against these problems, we explicitly load the DirectX components we need. If we were to implicitly link to Directlnput in DirectX 8, we would have added the din-putS.lib to our link line and used the following code:
IDirectlnputS *pDInput;
HRESULT hr = DirectInput8Create(hInstance, DIRECTINPUT_VERSION, IID_IDirectInput8,
(LPVOID*) & pDInput, 0);
if (FAILED(hr)) {
// handle error - initialization error }
The explicit DLL loading case effectively adds two more lines of code, but the application is now protected against dinput8.dll not being found, or of it being cor-rupt in some way.
typedef HRESULT (WINAPI* DirectInput8Create_PROC)
(HINSTANCE hinst, DWORD dwVersion, REFIID riidltf, LPVOID* ppvOut,
LPUNKNOWN punkOuter);
HMODULE hDInputLib = LoadLibrary( "dinput8.dll") ; if (! hDInputLib)
{
// handle error - DInput 8 not found. Is it installed incorrectly // or at all?
DirectInput8Create_PROC diCreate;
diCreate = (DirectInput8Create_PROC)
GetProcAddress(hDInputLib, "DirectlnputSCreate") ; if (! diCreate)
{
// handle error - DInput 8 exists, but the function can't be // found.
HRESULT hr = (diCreate) (hlnstance, DIRECTINPUT_VERSION, I ID_IDirect Inputs,
(LPVOID*) &mDirectInput, NULL);
if (FAILED(hr)) {
// handle error - initialization error
First, a function pointer typedef is created that reflects the function Direct-lnputSCreate. The DLL is then loaded using LoadLibrary. If the dinput8.dll was loaded successfully, we then attempt to find the function DirectlnputSCreate using GetProcAddress. GetProcAddress returns a pointer to the function if it is found, or NULL if the function cannot be found. We then check to make sure the function pointer is valid. Finally, we call DirectlnputSCreate through the function pointer to initialize Directlnput.
If there were more functions that needed to be retrieved from the DLL, a func-tion pointer typedefand variable would be declared for each. It might be sufficient to only check for NULL when mapping the first function pointer using GetProcAddress.
However, as more error handling is usually not a bad thing, checking every Get-ProcAddress for a successful non-NULL return is probably a good thing to do.
Using OS-Specific Features _
Another issue that explicit DLL loading can resolve is when an application wants to take advantage of a specific API function if it is available. There is an extensive num-ber of extended functions ending in "Ex" that are supported under Windows NT or 2000, and not available in Windows 95 or 98. These extended functions usually pro-vide more information or additional functionality than the original functions do .
An example of this is the CopyFileEx function, which provides the ability to can-cel a long file copy operation. Instead of calling it directly, kernel32.dll can be loaded using LoadLibrary and the function again mapped with GetProcAddress. If we load kernel32.dll and find CopyFileEx, we use it. If we don't find it, we can use the regular CopyFile function. One other problem that must be avoided in this case is that Copy-FileEx is really only a #define replacement in the winbase.h header file that is replaced with CopyFileExA or CopyFileExW if compiling for ASCII or wide Unicode charac-ters, respectively.
typedef BOOL (WINAPI *CopyFileEx_PROC) (LPCTSTR IpExistingFileName, LPCTSTR IpNewFileName , LPPROGRESS_ROUTINE IpProgressRoutine, LPVOID IpData, LPBOOL pbCancel, DWORD dwCopyFlags) ;
HMODULE hKerne!32 = LoadLibrary("kernel32.dH") ; if (!hKerne!32)
{
// handle error - kernel32.dll not found. Wow! That's really bad }
CopyFileEx_PROC pfnCopyFileEx;
pfnCopyFileEx = (CopyFileEx_PROC) GetProcAddress(hKernel32,
"CopyFileExA") ; BOOL bReturn;
if (pfnCopyFileEx) {
/ / use CopyFileEx to copy the file
bReturn = (pfnCopyFileEx) (pExistingFile, pDestinationFile, ...);
else
// use the regular CopyFile function
bReturn = CopyFilefpExistingFile, pDestinationFile, FALSE);
The use of LoadLibrary and GetProcAddress can also be applied to game DLLs.
One example of this is the graphics support in a game engine currently under devel-opment at Ensemble Studios, where graphics support for Direct3D and OpenGL has been broken out into separate DLLs that are explicitly loaded as necessary. If Direct3D graphics support is needed, the Direct3D support DLL is loaded with LoadLibrary and the exported functions are mapped using GetProcAddress. This setup keeps the main executable free from having to link implicitly with either dddS.lib or opengl32.lib.
However, the supporting Direct3D DLL links implicitly with dddS.lib, and the supporting OpenGL DLL links implicitly with opengl32. lib. This explicit loading of the game's own DLLs by the main executable, and implicit loading by each graphics subsystem solves several problems. First, if an attempt to load either library fails, it's likely that that particular graphics subsystem files cannot be found or are corrupt. The main program can then handle the error gracefully. The other problem that this solves, which is more of an issue with OpenGL than Direct3D, is that if the engine were to link explicitly to OpenGL, it would need a typedef and function pointer for every OpenGL function it used. The implicit linking to the support DLL solves this problem.
Summary
Explicit linking can act as a barrier against a number of common DLL problems that are encountered under Windows, including missing DLLs, or versions of DLLs that aren't compatible with an application. While not a panacea, it can at least put the application in control and allow any error to be handled gracefully instead of with a cryptic error message or an outright crash.