The DLL s Entry-Point Function

[Previous] [Next]

A DLL can have a single entry-point function. The system calls this entry-point function at various times, which I'll discuss shortly. These calls are informational and are usually used by a DLL to perform any per-process or per-thread initialization and cleanup. If your DLL doesn't require these notifications, you do not have to implement this function in your DLL source code. For example, if you create a DLL that contains only resources, you do not need to implement this function. If you do want to receive notifications in your DLL, you can implement an entry-point function that looks like this:

 BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad) { switch (fdwReason) { case DLL_PROCESS_ATTACH: // The DLL is being mapped into the process's address space. break; case DLL_THREAD_ATTACH: // A thread is being created. break; case DLL_THREAD_DETACH: // A thread is exiting cleanly. break; case DLL_PROCESS_DETACH: // The DLL is being unmapped from the process's address space. break; } return(TRUE); // Used only for DLL_PROCESS_ATTACH } 

NOTE
The function name DllMain is case-sensitive. Many developers accidentally call the function DLLMain instead. This is an easy mistake to make since the term DLL is frequently represented in all capital letters. If you call the entry-point function anything but DllMain, your code will compile and link; however, your entry-point function will never be called and your DLL will never initialize.

The hinstDll parameter contains the instance handle of the DLL. Like the hinstExe parameter to (w)WinMain, this value identifies the virtual memory address of where the DLL's file image was mapped in the process's address space. You usually save this parameter in a global variable so you can use it in calls that load resources, such as DialogBox and LoadString. The last parameter, fImpLoad, is nonzero if the DLL is implicitly loaded and zero if the DLL is explicitly loaded.

The fdwReason parameter indicates why the system is calling the function. This parameter can have one of four values: DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH, DLL_THREAD_ATTACH, or DLL_THREAD_DETACH. These are discussed in the following sections.

NOTE
You must remember that DLLs use DllMain functions to initialize themselves. When your DllMain executes, other DLLs in the same address space probably haven't executed their DllMain functions yet. This means that they have not initialized, so you should avoid calling functions imported from other DLLs. In addition, you should avoid calls to LoadLibrary(Ex) and FreeLibrary from inside DllMain because these functions can create dependency loops.

The Platform SDK documentation states that your DllMain should perform only simple initialization such as setting up thread local storage (discussed in Chapter 21), creating kernel objects, and opening files. You must also avoid calls to User, Shell, ODBC, COM, RPC, and socket functions (or functions that call these functions) because their DLLs might not have initialized yet or the functions might call LoadLibrary(Ex) internally, again creating a dependency loop.

Also be aware that the same problems exist if you create global or static C++ objects because the constructor or destructor for these objects is called at the same time as your DllMain function.

The DLL_PROCESS_ATTACH Notification

When a DLL is first mapped into a process's address space, the system calls the DLL's DllMain function, passing it a value of DLL_PROCESS_ATTACH for the fdwReason parameter. This happens only when the DLL's file image is first mapped. If a thread later calls LoadLibrary(Ex) for a DLL that is already mapped into the process's address space, the operating system simply increments the DLL's usage count; it does not call the DLL's DllMain function again with a value of DLL_PROCESS_ATTACH.

When processing DLL_PROCESS_ATTACH, a DLL should perform any process-relative initialization required by functions contained within the DLL. For example, the DLL might contain functions that need to use their own heap (created in the process's address space). The DLL's DllMain function can create this heap by calling HeapCreate during its processing of the DLL_PROCESS_ATTACH notification. The handle to the created heap can be saved in a global variable that the DLL functions have access to.

When DllMain processes a DLL_PROCESS_ATTACH notification, DllMain's return value indicates whether the DLL's initialization was successful. If, for example, the call to HeapCreate was successful, DllMain should return TRUE. If the heap could not be created, it should return FALSE. For any of the other fdwReason values—DLL_PROCESS_DETACH, DLL_THREAD_ATTACH, and DLL_THREAD_DETACH—the system ignores the return value from DllMain.

Of course, some thread in the system must be responsible for executing the code in the DllMain function. When a new process is created, the system allocates the process's address space and then maps the .exe file image and all of the required DLL file images into the process's address space. Then it creates the process's primary thread and uses this thread to call each of the DLL's DllMain functions with a value of DLL_PROCESS_ATTACH. After all of the mapped DLLs have responded to this notification, the system causes the process's primary thread to begin executing the executable module's C/C++ run-time startup code, followed by the executable module's entry-point function (main, wmain, WinMain, or wWinMain). If any of the DLL's DllMain functions return FALSE, indicating unsuccessful initialization, the system terminates the entire process, removing all the file images from its address space and displaying a message box to the user stating that the process could not be started. The message box for Windows 2000 is shown below, followed by the message box for Windows 98.

click to view at full size.

Now let's look at what happens when a DLL is loaded explicitly. When a thread in a process calls LoadLibrary(Ex), the system locates the specified DLL and maps the DLL into the process's address space. Then the system calls the DLL's DllMain function with a value of DLL_PROCESS_ATTACH, using the thread that placed the call to LoadLibrary(Ex). After the DLL's DllMain function has processed the notification, the system allows the call to LoadLibrary(Ex) to return, and the thread continues processing as normal. If the DllMain function returns FALSE, indicating that the initialization was unsuccessful, the system automatically unmaps the DLL's file image from the process's address space and the call to LoadLibrary(Ex) returns NULL.

The DLL_PROCESS_DETACH Notification

When a DLL is unmapped from a process's address space, the system calls the DLL's DllMain function, passing it an fdwReason value of DLL_PROCESS_DETACH. A DLL should perform any process-relative cleanup when it processes this value. For example, a DLL might call HeapDestroy to destroy a heap that it created during the DLL_PROCESS_ATTACH notification. Note that if a DllMain function returns FALSE when it receives a DLL_PROCESS_ATTACH notification, the DllMain function is not called with a DLL_PROCESS_DETACH notification. If the DLL is being unmapped because the process is terminating, the thread that calls the ExitProcess function is responsible for executing the DllMain function's code. Under normal circumstances, this is the application's primary thread. When your entry-point function returns to the C/C++ run-time library's startup code, the startup code explicitly calls the ExitProcess function to terminate the process.

If the DLL is being unmapped because a thread in the process called FreeLibrary or FreeLibraryAndExitThread, the thread that made the call executes the DllMain function code. If FreeLibrary is used, the thread does not return from this call until after the DllMain function has finished executing the DLL_PROCESS_DETACH notification.

Note that a DLL can prevent the process from dying. For example, DllMain might enter an infinite loop when it receives the DLL_PROCESS_DETACH notification. The operating system actually kills the process only after every DLL has completed processing the DLL_PROCESS_DETACH notification.

NOTE
If a process terminates because some thread in the system calls TerminateProcess, the system does not call the DLL's DllMain function with a value of DLL_PROCESS_DETACH. This means that any DLLs mapped into the process's address space do not have a chance to perform any cleanup before the process terminates. This can result in the loss of data. You should use the TerminateProcess function only as a last resort!

Figure 20-2 shows the steps that are performed when a thread calls LoadLibrary. Figure 20-3 shows the steps that are performed when a thread calls FreeLibrary.

click to view at full size.

Figure 20-2. The steps performed by the system when a thread calls LoadLibrary

click to view at full size.

Figure 20-3. The steps performed by the system when a thread calls FreeLibrary

The DLL_THREAD_ATTACH Notification

When a thread is created in a process, the system examines all of the DLL file images currently mapped into the process's address space and calls each one's DllMain function with a value of DLL_THREAD_ATTACH. This tells all the DLLs to perform any per-thread initialization. The newly created thread is responsible for executing the code in all of the DLLs' DllMain functions. Only after all the DLLs have had a chance to process this notification does the system allow the new thread to begin executing its thread function.

If a process already has several threads running in it when a new DLL is mapped into its address space, the system does not call the DLL's DllMain function with a value of DLL_THREAD_ATTACH for any of the existing threads. It calls the DLL's DllMain with a value of DLL_THREAD_ATTACH only if the DLL is mapped into the process's address space when a new thread is created.

Also note that the system does not call any DllMain functions with a value of DLL_THREAD_ATTACH for the process's primary thread. Any DLLs that are mapped into the process's address space when the process is first invoked receive the DLL_PROCESS_ATTACH notification but not the DLL_THREAD_ATTACH notification.

The DLL_THREAD_DETACH Notification

The preferred way for a thread to terminate is to have its thread function return. This causes the system to call ExitThread to kill the thread. ExitThread tells the system that the thread wants to die, but the system does not kill the thread right away. Instead, it takes the soon-to-be-dead thread and has it call all the mapped DLL's DllMain functions with a reason of DLL_THREAD_DETACH. This notification tells all the DLLs to perform any per-thread cleanup. For example, the DLL version of the C/C++ run-time library frees the data block that it uses to manage multithreaded applications.

Note that a DLL can prevent the thread from dying. For example, the DllMain function can enter an infinite loop when it receives the DLL_THREAD_DETACH notification. The operating system actually kills the thread only after every DLL has completed processing the DLL_THREAD_DETACH notification.

NOTE
If a thread terminates because a thread in the system calls TerminateThread, the system does not call all of the DLLs' DllMain functions with a value of DLL_THREAD_DETACH. This means that any DLLs mapped into the process's address space do not have a chance to perform any cleanup before the thread terminates. This can result in the loss of data. As with TerminateProcess, use the TerminateThread function only as a last resort!

If any threads are still running when the DLL is detached, DllMain is not called with DLL_THREAD_DETACH for any of the threads. You might want to check for this in your DLL_PROCESS_DETACH processing so that you can perform any necessary cleanup.

Because of the rules stated above, the following situation might occur: A thread in a process calls LoadLibrary to load a DLL, causing the system to call the DLL's DllMain function with a value of DLL_PROCESS_ATTACH. (Note that the DLL_THREAD_ATTACH notification is not sent for this thread.) Next, the thread that loaded the DLL exits, causing the DLL's DllMain function to be called again, this time with a value of DLL_THREAD_DETACH. Notice that the DLL is notified that the thread is detaching even though it never received a DLL_THREAD_ATTACH notifying the library that the thread had attached. For this reason, you must be extremely careful when you perform any thread-specific cleanup. Fortunately, most programs are written so that the thread that calls LoadLibrary is the same thread that calls FreeLibrary.

Serialized Calls to DllMain

The system serializes calls to a DLL's DllMain function. To understand what this means, consider the following scenario. A process has two threads, Thread A and Thread B. The process also has a DLL, SomeDLL.dll, mapped into its address space. Both threads are about to call the CreateThread function to create two more threads, Thread C and Thread D.

When Thread A calls CreateThread to create Thread C, the system calls SomeDLL.dll's DllMain function with a value of DLL_THREAD_ATTACH. While Thread C executes the code in the DllMain function, Thread B calls CreateThread to create Thread D. The system must call DllMain again with a value of DLL_THREAD_ATTACH, this time having Thread D execute the code. However, calls to DllMain are serialized by the system, and the system suspends Thread D until Thread C has completely processed the code in DllMain and returned.

After Thread C finishes processing DllMain, it can begin executing its thread function. Now the system wakes up Thread D and allows it to process the code in DllMain. When it returns, Thread D begins processing its thread function.

Normally, you don't even think about this DllMain serialization. The reason I'm making a big deal out of it is that I worked with someone who had a bug in his code caused by DllMain serialization. His code looked something like the code below.

 BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad) { HANDLE hThread; DWORD dwThreadId; switch (fdwReason) { case DLL_PROCESS_ATTACH: // The DLL is being mapped into the process's address space. // Create a thread to do some stuff. hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId); // Suspend our thread until the new thread terminates. WaitForSingleObject(hThread, INFINITE); // We no longer need access to the new thread. CloseHandle(hThread); break; case DLL_THREAD_ATTACH: // A thread is being created. break; case DLL_THREAD_DETACH: // A thread is exiting cleanly. break; case DLL_PROCESS_DETACH: // The DLL is being unmapped from the process's address space. break; } return(TRUE); } 

It took us several hours to discover the problem with this code. Can you see it? When DllMain receives a DLL_PROCESS_ATTACH notification, a new thread is created. The system must call DllMain again with a value of DLL_ THREAD_ATTACH. However, the new thread is suspended because the thread that caused the DLL_PROCESS_ATTACH notification to be sent to DllMain has not finished processing. The problem is the call to WaitForSingleObject. This function suspends the currently executing thread until the new thread terminates. However, the new thread never gets a chance to run, let alone terminate, because it is suspended—waiting for the current thread to exit the DllMain function. What we have here is a deadlock situation. Both threads are suspended forever!

When I first started thinking about how to solve this problem, I discovered the DisableThreadLibraryCalls function:

 BOOL DisableThreadLibraryCalls(HINSTANCE hinstDll); 

DisableThreadLibraryCalls tells the system that you do not want DLL_THREAD_ATTACH and DLL_THREAD_DETACH notifications sent to the specified DLL's DllMain function. It seemed reasonable to me that, if we told the system not to send DLL notifications to the DLL, the deadlock situation would not occur. However, when I tested my solution, which follows, I soon discovered that it didn't solve the problem.

 BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad) { HANDLE hThread; DWORD dwThreadId; switch (fdwReason) { case DLL_PROCESS_ATTACH: // The DLL is being mapped into the process's address space. // Prevent the system from calling DllMain // when threads are created or destroyed. DisableThreadLibraryCalls(hinstDll); // Create a thread to do some stuff. hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId); // Suspend our thread until the new thread terminates. WaitForSingleObject(hThread, INFINITE); // We no longer need access to the new thread. CloseHandle(hThread); break; case DLL_THREAD_ATTACH: // A thread is being created. break; case DLL_THREAD_DETACH: // A thread is exiting cleanly. break; case DLL_PROCESS_DETACH: // The DLL is being unmapped from the process's address space. break; } return(TRUE); } 

Upon further research, I discovered the problem. When a process is created, the system also creates a mutex object. Each process has its own mutex object—multiple processes do not share the mutex object. This mutex object synchronizes all of a process's threads when the threads call the DllMain functions of the DLLs mapped into the process's address space.

When the CreateThread function is called, the system first creates the thread kernel object and the thread's stack. Then it internally calls the WaitForSingleObject function, passing the handle of the process's mutex object. Once the new thread has ownership of the mutex, the system makes the new thread call each DLL's DllMain function with a value of DLL_THREAD_ATTACH. Only then does the system call ReleaseMutex to relinquish ownership of the process's mutex object. Because the system works this way, adding the call to DisableThreadLibraryCalls does not prevent the threads from deadlocking. The only way I could think of to prevent the threads from being suspended was to redesign this part of the source code so that WaitForSingleObject is not called inside any DLL's DllMain function.

DllMain and the C/C++ Run-Time Library

In the discussion of the DllMain function above, I have assumed that you are using the Microsoft Visual C++ compiler to build your DLL. When you write a DLL, you'll probably need some startup assistance from the C/C++ run-time library. For example, say that you are building a DLL that contains a global variable and that this global variable is an instance of a C++ class. Before you can safely use the global variable inside your DllMain function, the variable must have its constructor called. This is a job for the C/C++ run-time library's DLL startup code.

When you link your DLL, the linker embeds the address of the DLL's entry-point function in the resulting DLL file image. You specify the address of this function using the linker's /ENTRY switch. By default, when you use Microsoft's linker and specify the /DLL switch, the linker assumes that the entry function is called _DllMainCRTStartup. This function is contained inside the C/C++ run-time's library file and is statically linked in your DLL file's image when you link your DLL. (The function is statically linked even if you use the DLL version of the C/C++ run-time library.)

When your DLL file image is mapped into a process's address space, the system actually calls this _DllMainCRTStartup function instead of your DllMain function. The _DllMainCRTStartup function initializes the C/C++ run-time library and ensures that any global or static C++ objects are constructed when _DllMainCRTStartup receives the DLL_PROCESS_ATTACH notification. After any C/C++ run-time initialization has been performed, the _DllMainCRTStartup function calls your DllMain function.

When the DLL receives a DLL_PROCESS_DETACH notification, the system again calls the _DllMainCRTStartup function. This time, the function calls your DllMain function, and when DllMain returns, _DllMainCRTStartup calls any destructors for any global or static C++ objects in the DLL. The _DllMainCRTStartup function doesn't do any special processing when it receives a DLL_THREAD_ATTACH notification. But for a DLL_THREAD_DETACH notification, the C/C++ run-time frees a thread's tiddata memory block, if one exists. Normally, however, this tiddata memory block should not exist because a properly written thread function returns to the C/C++ run-time's _threadstartex function (discussed in Chapter 6) which internally calls _endthreadex, which frees the memory block before the thread attempts to call ExitThread.

However, consider the case in which an application written in Pascal calls functions in a DLL written in C/C++. In this scenario, the Pascal application creates a thread and does not use _beginthreadex. So the thread knows nothing about the C/C++ run-time library. Now the thread calls a function in the DLL, which in turn calls a C run-time function. As you'll recall, the C run-time function creates a tiddata memory block for this thread and associates it with the thread on the fly. This means that a Pascal application can create threads that successfully call C run-time functions! When the Pascal-written thread function returns, ExitThread is called. The C/C++ run-time library DLL receives the DLL_THREAD_DETACH notification and frees the tiddata memory block so that no memory leaks occur. This stuff was pretty well thought out, huh?

I mentioned earlier that you do not have to implement a DllMain function in your DLL's source code. If you don't have your own DllMain function, you can use the C/C++ run-time library's implementation of a DllMain function, which looks like this (if you're statically linking to the C/C++ run-time library):

 BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad) { if (fdwReason == DLL_PROCESS_ATTACH) DisableThreadLibraryCalls(hinstDll); return(TRUE); } 

When the linker links your DLL, it links the C/C++ run-time library's implementation of the DllMain function if the linker cannot find a DllMain function in your DLL's .obj files. If you don't supply your own DllMain function, the C/C++ run-time library rightfully assumes that you don't care about DLL_THREAD_ATTACH and DLL_THREAD_DETACH notifications. To improve the performance of creating and destroying threads, DisableThreadLibraryCalls is called.



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