Delay-Loading a DLL

[Previous] [Next]

Microsoft Visual C++ 6.0 offers a fantastic new feature to make working with DLLs easier: delay-load DLLs. A delay-load DLL is a DLL that is implicitly linked but not actually loaded until your code attempts to reference a symbol contained within the DLL. Delay-load DLLs are helpful in these situations:

  • If your application uses several DLLs, its initialization time might be slow because the loader maps all of the required DLLs into the process's address space. One way to alleviate this problem is to spread out the loading of the DLLs as the process executes. Delay-load DLLs let you accomplish this easily.
  • If you call a new function in your code and then try to run your application on an older version of the system in which the function does not exist, the loader reports an error and does not allow the application to run. You need a way to allow your application to run and then, if you detect (at run time) that the application is running on an older system, you don't call the missing function. For example, let's say that an application wants to use the PSAPI functions when running on Windows 2000 and the ToolHelp functions (like Process32Next) when running on Windows 98. When the application initializes, it calls GetVersionEx to determine the host operating system and properly calls the appropriate functions. Attempting to run this application on Windows 98 causes the loader to display an error message because the PSAPI.dll module doesn't exist on Windows 98. Again, delay-load DLLs let you solve this problem easily.

I've spent quite a bit of time playing with the delay-load DLL feature of Visual C++ 6.0, and I must say that Microsoft has done an excellent job in implementing it. It offers many features and works equally well on both Windows 98 and Windows 2000.

Let's start with the easy stuff: getting delay-load DLLs to work. First, you create a DLL just as you normally would. You also create an executable as you normally would but you do have to change a couple of linker switches and relink the executable. Here are the two linker switches you need to add:

     /Lib:DelayImp.lib
     /DelayLoad:MyDll.dll

The /Lib switch tells the linker to embed a special function, _ _delayLoadHelper, into your executable. The second switch tells the linker the following things:

  • Remove MyDll.dll from the executable module's import section so that the operating system loader does not implicitly load the DLL when the process initializes.
  • Embed a new Delay Import section (called .didata) in the executable indicating which functions are being imported from MyDll.dll.
  • Resolve calls to the delay-loaded functions by having calls jump to the _ _delayLoadHelper function.

When the application runs, a call to a delay-loaded function actually calls the _ _delayLoadHelper function instead. This function references the special Delay Import section and knows to call LoadLibrary followed by GetProcAddress. Once the address of the delay-loaded function is obtained, _ _delayLoadHelper fixes up calls to that function so future calls go directly to the delay-loaded function. Note that other functions in the same DLL still have to be fixed up the first time you call them. Also note that you can specify the /DelayLoad linker switch multiple times—once for every DLL that you want to delay-load.

OK, that's it. It's that simple! It is, really. But you should also consider a couple of other issues. Normally, when the operating system loader loads your executable, it tries to load the required DLLs. If a DLL can't be loaded, the loader displays an error message. But for delay-loaded DLLs, the existence of the DLL is not checked at initialization time. If the DLL can't be found when a delay-loaded function is called, the _ _delayLoadHelper function raises a software exception. You can trap this exception using structured exception handling (SEH) and keep your application running. If you don't trap the exception, your process is terminated. (SEH is discussed in Chapters 23, 24, and 25.)

Another problem can occur when _ _delayLoadHelper does find your DLL but the function you're trying to call isn't in the DLL. This can happen if the loader finds an old version of the DLL, for example. In this case, _ _delayLoadHelper also raises a software exception and the same rules apply. The sample application presented in the next section shows how to properly write the SEH code to handle these errors.

You'll notice a lot of other stuff in the code that has nothing to do with SEH and error handling. It has to do with additional features that are available when you use delay-load DLLs. I'll describe these features shortly. If you don't use the more advanced features, you can delete this additional code.

As you can see, the Visual C++ team has defined two software exception codes: VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND) and VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND). These indicate that the DLL module was not found and that the function was not found, respectively. My exception filter function, DelayLoadDllExceptionFilter, checks for these two exception codes. If neither code is thrown, the filter returns EXCEPTION_CONTINUE_SEARCH, as any good filter should. (Never swallow exceptions that you don't know how to handle.) However, if one of these codes is thrown, the _ _delayLoadHelper function provides a pointer to a DelayLoadInfo structure containing some additional information. The DelayLoadInfo structure is defined in Visual C++'s DelayImp.h file as follows:

 typedef struct DelayLoadInfo { DWORD cb; // Size of structure PCImgDelayDescr pidd; // Raw data (everything is there) FARPROC * ppfn; // Points to address of function to load LPCSTR szDll; // Name of dll DelayLoadProc dlp; // Name or ordinal of procedure HMODULE hmodCur; // hInstance of loaded library FARPROC pfnCur; // Actual function that will be called DWORD dwLastError;// Error received } DelayLoadInfo, * PDelayLoadInfo; 

This data structure is allocated and initialized by the _ _delayLoadHelper function. As the function progresses through its work of dynamically loading the DLL and getting the address of the called function, it populates the members of this structure. Inside your SEH filter, the szDll member points to the name of the DLL you're attempting to load and the function you're attempting to look up is in the dlp member. Since you can look up functions by ordinal or by name, the dlp member looks like this:

 typedef struct DelayLoadProc { BOOL fImportByName; union { LPCSTR szProcName; DWORD dwOrdinal; }; } DelayLoadProc; 

If the DLL successfully loads but does not contain the desired function, you might also look at the hmodCur member to see the memory address where the DLL is loaded. You can also check the dwLastError member to see what error caused the exception to be raised, but this probably isn't necessary for an exception filter because the exception code tells you what happened. The pfnCur member contains the address of the desired function. This is always set to NULL in the exception filter because _ _delayLoadHelper couldn't find the address of the function.

Of the remaining members, cb is for versioning, pidd points to the section embedded in the module that contains the list of delay-load DLLs and functions, and ppfn is the address where the function's address will go if the function is found. These last two members are used by the _ _delayLoadHelper function internally. They are for super-advanced use; it is extremely unlikely that you will ever have to examine or understand them.

So far, I've explained the basics of using delay-load DLLs and properly recovering from error conditions. However, Microsoft's implementation of delay-load DLLs goes beyond what I have discussed so far. Your application can unload a delay-loaded DLL, for example. Let's say that your application requires a special DLL to print the user's document. This DLL is a perfect candidate to be a delay-load DLL because most of the time it probably won't be used. However, if the user chooses the Print command, you can call a function in the DLL and it will automatically load. This is great, but after the document is printed, the user probably won't print another document immediately, so you can unload the DLL and free system resources. If the user decides to print another document, the DLL will again be loaded on demand.

To unload a delay-loaded DLL, you must do two things. First, you must specify an additional linker switch (/Delay:unload) when you build your executable file. Second, you must modify your source code and place a call to the _ _FUnloadDelayLoadedDLL function at the point where you want the DLL to be unloaded:

 BOOL _ _FUnloadDelayLoadedDLL(PCSTR szDll); 

The /Delay:unload linker switch tells the linker to place another section inside the file. This section contains the information necessary to reset the functions you have already called so that they again call the _ _delayLoadHelper function. When you call _ _FUnloadDelayLoadedDLL, you pass it the name of the delay-load DLL that you want to unload. The function then goes to the unload section in the file and resets all of the DLL's function addresses. Then _ _FUnloadDelayLoadedDLL calls FreeLibrary to unload the DLL.

Let me point out a few important items. First, make sure that you don't call FreeLibrary yourself to unload the DLL or the function's address will not be reset; this will cause an access violation the next time you attempt to call a function in the DLL. Second, when you call _ _FUnloadDelayLoadedDLL, the DLL name you pass should not include a path and the letters in the name must be the same case that was used when you passed the DLL name to the /DelayLoad linker switch; otherwise, _ _FUnloadDelayLoadedDLL will fail. Third, if you never intend to unload a delay-loaded DLL, do not specify the /Delay:unload linker switch and your executable file will be smaller in size. Lastly, if you call _ _FUnloadDelayLoadedDLL from a module that was not built with the /Delay:unload switch, nothing bad happens: _ _FUnloadDelayLoadedDLL simply does nothing and returns FALSE.

Another feature of the delay-load DLLs is that by default, the functions that you call are bindable to memory addresses where the system thinks the function will be in a process's address. (I'll discuss binding later in this chapter.) Since creating bindable delay-load DLL sections makes your executable file bigger, the linker also supports a /Delay:nobind switch. Since binding is generally preferred, most applications should not use this linker switch.

The last feature of delay-load DLLs is for advanced users and really shows Microsoft's attention to detail. As the _ _delayLoadHelper function executes, it can call hook functions that you provide. These functions receive notifications of _ _delayLoadHelper's progress and notifications of errors. In addition, these functions can override how the DLL is loaded and how the function's virtual memory address is obtained.

To get the notification or override behavior, you must do two things to your source code. First, you must write a hook function that looks like the DliHook function in Figure 20-6. The DliHook skeleton function does not affect _ _delayLoadHelper's operation. To alter the behavior, start with the DliHook function and then modify it as necessary. Then tell _ _delayLoadHelper the address of the function.

Inside the DelayImp.lib static-link library, two global variables are defined: _ _pfnDliNotifyHook and _ _pfnDliFailureHook. Both of these variables are of type PfnDliHook:

 typedef FARPROC (WINAPI *PfnDliHook)( unsigned dliNotify, PDelayLoadInfo pdli); 

As you can see, this is a function data type and matches the prototype of my DliHook function. Inside DelayImp.lib, the two variables are initialized to NULL, which tells _ _delayLoadHelper not to call any hook functions. To have your hook function called, you must set one of these variables to your hook function's address. In my code, I simply add these two lines of code at global scope:

 PfnDliHook _ _pfnDliNotifyHook = DliHook; PfnDliHook _ _pfnDliFailureHook = DliHook; 

As you can see, _ _delayLoadHelper actually works with two callback functions. It calls one to report notifications and the other to report failures. Since the prototypes are identical for both functions, and the first parameter, dliNotify, tells why the function is being called, I always make life simpler by creating a single function and setting both variables to point to my one function.

This new delay-load DLL feature of Visual C++ 6.0 is pretty cool, and I know a lot of developers who wish that they had this feature years ago. I can think of a lot of applications (especially Microsoft applications) that will take advantage of this mechanism.

The DelayLoadApp Sample Application

The DelayLoadApp application ("20 DelayLoadApp.exe"), listed in Figure 20-6, shows everything you need to do to take full advantage of delay-load DLLs. For demonstration purposes, a simple DLL is required; the code for that is in the 20-DelayLoadLib directory.

Since the application loads the "20 DelayLoadLib" module, the loader does not map this module into the process's address space when you run the application. Inside the application, I periodically call the IsModuleLoaded function. This function simply displays a message box notifying you whether a module is loaded into the process's address space. When the application first starts, the "20 DelayLoadLib" module is not loaded, causing the message box in Figure 20-4 to appear.

click to view at full size.

Figure 20-4. DelayLoadApp indicating that the "20 DelayLoadLib" module is not loaded

The application then calls a function imported from the DLL, which causes the _ _delayLoadHelper function to automatically load the DLL. When the function returns, the message box in Figure 20-5 is displayed.

click to view at full size.

Figure 20-5. DelayLoadApp indicating that the "20 DelayLoadLib" module is loaded

When this message box is dismissed, another function in the DLL is called. Since this function is in the same DLL, the DLL does not get loaded in the address space again, but the address of this new function is resolved and called.

At this point, _ _FUnloadDelayLoadedDLL is called; it unloads the "20 DelayLoadLib" module. Again, the call to IsModuleLoaded shows the message box in Figure 20-4. Finally, an imported function is again called, which reloads the "20 DelayLoadLib" module, causing the last call to IsModuleLoaded to show the message box in Figure 20-5.

If all is OK, the application will work as I've described. However, if you delete the "20 DelayLoadLib" module before running the application or if the module doesn't contain one of the imported functions, exceptions will be raised. The sample code shows how to recover "gracefully" (so to speak) from this situation.

Finally, the application shows how to properly set a delay load hook function. My skeleton DliHook function doesn't do anything of interest. However, it does trap various notifications and shows what you can do when you receive these notifications.

Figure 20-6. The DelayLoadApp sample application

DelayLoadApp.cpp

 /****************************************************************************** Module: DelayLoadApp.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\CmnHdr.h" /* See Appendix A. */ #include <Windowsx.h> #include <tchar.h> /////////////////////////////////////////////////////////////////////////////// #include <Delayimp.h> // For error handling & advanced features #include "..\20-DelayLoadLib\DelayLoadLib.h" // My DLL function prototypes /////////////////////////////////////////////////////////////////////////////// // Statically link _ _delayLoadHelper/_ _FUnloadDelayLoadedDLL #pragma comment(lib, "Delayimp.lib") // Tell the linker that my DLL should be delay loaded // Note the 2 (\") because the filename has a space in it #pragma comment(linker, "/DelayLoad:\"20 DelayLoadLib.dll\"") // Tell the linker that I want to be able to unload my DLL #pragma comment(linker, "/Delay:unload") // Tell the linker to make the delay load DLL unbindable // You usually want this, so I commented out this line //#pragma comment(linker, "/Delay:nobind") // The name of the Delay-Load module (only used by this sample app) TCHAR g_szDelayLoadModuleName[] = TEXT("20 DelayLoadLib"); /////////////////////////////////////////////////////////////////////////////// // Forward function prototype LONG WINAPI DelayLoadDllExceptionFilter(PEXCEPTION_POINTERS pep); /////////////////////////////////////////////////////////////////////////////// void IsModuleLoaded(PCTSTR pszModuleName) { HMODULE hmod = GetModuleHandle(pszModuleName); char sz[100]; #ifdef UNICODE wsprintfA(sz, "Module \"%S\" is %Sloaded.", pszModuleName, (hmod == NULL) ? L"not " : L""); #else wsprintfA(sz, "Module \"%s\" is %sloaded.", pszModuleName, (hmod == NULL) ? "not " : ""); #endif chMB(sz); } /////////////////////////////////////////////////////////////////////////////// int WINAPI _tWinMain(HINSTANCE hinstExe, HINSTANCE, PTSTR pszCmdLine, int) { // Wrap all calls to delay-load DLL functions inside SEH _ _try { int x = 0; // If you're in the debugger, try the new Debug.Modules menu item to // see that the DLL is not loaded prior to executing the line below IsModuleLoaded(g_szDelayLoadModuleName); x = fnLib(); // Attempt to call delay-load function // Use Debug.Modules to see that the DLL is now loaded IsModuleLoaded(g_szDelayLoadModuleName); x = fnLib2(); // Attempt to call delay-load function // Unload the delay-loaded DLL // NOTE: Name must exactly match /DelayLoad:(DllName) _ _FUnloadDelayLoadedDLL("20 DelayLoadLib.dll"); // Use Debug.Modules to see that the DLL is now unloaded IsModuleLoaded(g_szDelayLoadModuleName); x = fnLib(); // Attempt to call delay-load function // Use Debug.Modules to see that the DLL is loaded again IsModuleLoaded(g_szDelayLoadModuleName); } _ _except (DelayLoadDllExceptionFilter(GetExceptionInformation())) { // Nothing to do in here, thread continues to run normally } // More code can go here... return(0); } /////////////////////////////////////////////////////////////////////////////// LONG WINAPI DelayLoadDllExceptionFilter(PEXCEPTION_POINTERS pep) { // Assume we recognize this exception LONG lDisposition = EXCEPTION_EXECUTE_HANDLER; // If this is a Delay-load problem, ExceptionInformation[0] points // to a DelayLoadInfo structure that has detailed error info PDelayLoadInfo pdli = PDelayLoadInfo(pep->ExceptionRecord->ExceptionInformation[0]); // Create a buffer where we construct error messages char sz[500] = { 0 }; switch (pep->ExceptionRecord->ExceptionCode) { case VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND): // The DLL module was not found at run time wsprintfA(sz, "Dll not found: %s", pdli->szDll); break; case VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND): // The DLL module was found, but it doesn't contain the function if (pdli->dlp.fImportByName) { wsprintfA(sz, "Function %s was not found in %s", pdli->dlp.szProcName, pdli->szDll); } else { wsprintfA(sz, "Function ordinal %d was not found in %s", pdli->dlp.dwOrdinal, pdli->szDll); } break; default: // We don't recognize this exception lDisposition = EXCEPTION_CONTINUE_SEARCH; break; } if (lDisposition == EXCEPTION_EXECUTE_HANDLER) { // We recognized this error and constructed a message, show it chMB(sz); } return(lDisposition); } /////////////////////////////////////////////////////////////////////////////// // Skeleton DliHook function that does nothing interesting FARPROC WINAPI DliHook(unsigned dliNotify, PDelayLoadInfo pdli) { FARPROC fp = NULL; // Default return value // NOTE: The members of the DelayLoadInfo structure pointed // to by pdli shows the results of progress made so far. switch (dliNotify) { case dliStartProcessing: // Called when _ _delayLoadHelper attempts to find a DLL/function // Return 0 to have normal behavior or nonzero to override // everything (you will still get dliNoteEndProcessing) break; case dliNotePreLoadLibrary: // Called just before LoadLibrary // Return NULL to have _ _delayLoadHelper call LoadLibary // or you can call LoadLibrary yourself and return the HMODULE fp = (FARPROC) (HMODULE) NULL; break; case dliFailLoadLib: // Called if LoadLibrary fails // Again, you can call LoadLibary yourself here and return an HMODULE // If you return NULL, _ _delayLoadHelper raises the // ERROR_MOD_NOT_FOUND exception fp = (FARPROC) (HMODULE) NULL; break; case dliNotePreGetProcAddress: // Called just before GetProcAddress // Return NULL to have _ _delayLoadHelper call GetProcAddress // or you can call GetProcAddress yourself and return the address fp = (FARPROC) NULL; break; case dliFailGetProc: // Called if GetProcAddress fails // You can call GetProcAddress yourself here and return an address // If you return NULL, _ _delayLoadHelper raises the // ERROR_PROC_NOT_FOUND exception fp = (FARPROC) NULL; break; case dliNoteEndProcessing: // A simple notification that _ _delayLoadHelper is done // You can examine the members of the DelayLoadInfo structure // pointed to by pdli and raise an exception if you desire break; } return(fp); } /////////////////////////////////////////////////////////////////////////////// // Tell _ _delayLoadHelper to call my hook function PfnDliHook _ _pfnDliNotifyHook = DliHook; PfnDliHook _ _pfnDliFailureHook = DliHook; //////////////////////////////// End of File ////////////////////////////////// 

DelayLoadLib.cpp

 /****************************************************************************** Module: DelayLoadLib.cpp Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #include "..\CmnHdr.h" /* See Appendix A. */ #include <Windowsx.h> #include <tchar.h> /////////////////////////////////////////////////////////////////////////////// #define DELAYLOADLIBAPI extern "C" _ _declspec(dllexport) #include "DelayLoadLib.h" /////////////////////////////////////////////////////////////////////////////// int fnLib() { return(321); } /////////////////////////////////////////////////////////////////////////////// int fnLib2() { return(123); } //////////////////////////////// End of File ////////////////////////////////// 

DelayLoadLib.h

 /****************************************************************************** Module: DelayLoadLib.h Notices: Copyright (c) 2000 Jeffrey Richter ******************************************************************************/ #ifndef DELAYLOADLIBAPI #define DELAYLOADLIBAPI extern "C" _ _declspec(dllimport) #endif /////////////////////////////////////////////////////////////////////////////// DELAYLOADLIBAPI int fnLib(); DELAYLOADLIBAPI int fnLib2(); //////////////////////////////// End of File ////////////////////////////////// 



Programming Applications for Microsoft Windows
Programming Applications for Microsoft Windows (Microsoft Programming Series)
ISBN: 1572319968
EAN: 2147483647
Year: 1999
Pages: 193

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net