PBUninitialize

VOID PBUninitialize (DWORD dwMilliseconds, ULONG ulFlags)

Parameters

dwMilliseconds Maximum time to wait for other threads to complete their work before aborting cleanup.

ulFlags Zero or a combination of bit flags defined in PB.H. Currently only one flag is supported:

PB_UNINIT_SILENT Suppresses warning number 750.

Remarks

This function cleans up all Paul Bunyan library resources and disables any further use of library functions. Once this function has been called, the library cannot be re-initialized. Calling this function a second time will never have any effect.

PBUninitialize is a special purpose function that should not be used in most scenarios. The only case where itís necessary to call it is when linking the library into a DLL that is dynamically loaded and unloaded by one or more processes (e.g. in-proc COM server). Calling PBUninitialize when the DLL is unloading will do additional cleanup of resources not automatically performed by the operating system at that time.

The Paul Bunyan library uses global variables on a per process per module basis. These variables include several handles to kernel objects used for IPC. When linked into an executable module, these handles are never explicitly closed, resulting in an apparent resource leak. This is done so that a process can continue to log right up to the time that it terminates. An example of the utility of this is logging from exit list functions such as destructors of global objects. If cleanup was executed by code in our library, it would be done before the destructors of global objects in the module, unless we could be sure ours was the last function on the exit list. Thus logging from exit list functions would be impossible. It is important to note that these handles will always be closed by the operating system when the process terminates. There is never any real leaking of handles or other resources.

The situation is similar when the Paul Bunyan library is built into a dynamic link library which is implicitly linked to the processes that use it. Such a DLL is mapped into a processís address space when the process starts and unloaded when the process terminates. If two or more processes use the DLL simultaneously, a separate copy of the library global variables is created for each process. When a process terminates, all the handles it has allocated, including those in the library linked into the DLL, are closed by the operating system. Again, there is no chance of leaking resources.

Now letís get back to the situation where PBUninitialize is important. When a DLL using PB is explicitly loaded into a processís address space with the Win32 API function LoadLibrary, the process again gets a set of global variables declared in the PB library. When unloaded with FreeLibrary, the copy of the DLLís (and so PB libraryís) global variables are destroyed. But since the process itself is not going away, the OS does not clean up the handles it has allocated. If a DLL is loaded and unloaded repeatedly, a cumulative resource leak results until the process is terminated. Hence the need to call PBUninitialize.

In most cases where it is necessary to use PBUninitialize, it is sufficient to call it with a short timeout from the DllMain function when the DLL_PROCESS_DETACH notification is received. There are a few things to watch out for though.

If more than one thread is potentially using the PB library in the per process per module space, PBUninitialize should be called only after all but one thread have finished their work. The normal scenario in a multithreaded executable is to have the main thread wait for any other threads to complete their tasks before returning from the entry point function (usually main or WinMain). The same would be true for a DLL that has started worker threads: all threads should be ended before returning TRUE from DllMain after the DLL_PROCESS_DETACH notification is received. If multiple threads in a process are using functions in a DLL, the DLL should not be allowed to unload until all threads are done using it. If these coding practices are observed, PBUninitialize can be called with a timeout of zero from the end of the entry point function or from an exit list function such as a destructor of a global C++ object.

The dwMilliseconds parameter is supplied only because it is not always easy to guarantee that all threads have completed their work. If other threads are trying to use the Paul Bunyan library while a thread is attempting to uninitialize, the uninitializing thread will set a flag so that no other threads will attempt to use library resources, and then wait for the threads that were already using the resources to complete their tasks. If the timeout period elapses, PBUninitialize will abandon the attempt to clean up resources and report the occurrence to the event log. Further use of library functions will be impossible. It behaves this way because we didnít want there to be any chance that the PB code could have detrimental effects on our clientsí code, even if our code is not used exactly as we intended. The most direct implementation of cleanup would simply perform the cleanup and let any other threads trying to use the library resources at the same time take care of themselves. But using that method, there would either be a very slight chance that a thread would get stuck holding our mutex and hang every logger on the box until it went away or there would be a significant decrease in performance on every function in the library. We couldnít stomach there being even an astronomically small chance that our code would misbehave, and a performance hit on normal usage of our library for this special case function was equally unpalatable. If there is sufficient consumer interest, we might be persuaded to release a hard-core version of the library that guarantees cleanup at the expense of performance.

It is very important that the timeout value supplied in dwMilliseconds not be INFINITE. Because of various implementation details including those mentioned in the previous paragraph, PBUninitialize will not perform cleanup if a thread dies while executing library functions, for example if the TerminateThread API function is used. Normally TerminateThread is a bad idea anyway and it would have to be executed just at the wrong time since very little time is spent executing PB library functions. But it is possible for a thread to die and therefore not signal that itís done using library resources. If this happens, PBUninitialize will time out waiting for that thread to finish. If the timeout specified is INFINITE and a thread has died, PBUninitialize will never return and the calling thread will be hung. We recommend that the timeout be specified as zero. If uninitialization cannot be completed, notification message number 702 will be placed in the application event log. This could indicate either that an additional thread is attempting to use library resources when PBUninitialize is called or that a thread has died while using library resources. Either way, this indicates a potential error in the library clientís code. By increasing the timeout value (100 milliseconds is a long time in terms of waiting for threads to complete library functions) it should be possible to distinguish between a race condition and a terminated thread.

Specifying PB_UNINIT_SILENT for the ulFlags parameter prevents event log notification that a library function has been called after PBUninitialize. Once this function has been called, it is not possible to use library functions; they will return having done nothing. An example where this might occur is if PBUninitialize is called from DllMain but a destructor of a global C++ object tries to log out a message. Specifying PB_UNIT_SILENT will not suppress error notifications during cleanup such as timing out as discussed above

If it is desired to log from exit list functions such as destructors of global C++ objects, PBUninitialize can be called during exit list processing. Exit list functions are processed in LIFO order, so to enable logging up to the very last minute, PBUninitialize should be the first function added to the exit list. In a C program, this is simply achieved by specifying the address of PBUninitialize in a call to the run-time library function atexit placed at the top of the entry point function. In C++ things are a little more complicated. Destructors of global objects are added to the exit list in the order that the objects are encountered. In a single file project, having PBUninitialize called last would be as simple as instantiating a global object at the top of the file which calls PBUninitialize in its destructor, as in the following sample.

#include <pb.h>

class CUninitializer
    {
    public:
    ~CUninitializer()
        {
        PBUninitialize(0, 0);
        }
    };

CUninitializer    uninitializer;

//Instantiate other global C++ objects here.
//Implement exports and helper functions here.

BOOL WINAPI DllMain(HINSTANCE, DWORD, LPVOID)
    {
    return TRUE;
    }

Of course life is never that simple; non-trivial applications require multiple source files, static library modules, etc. In that case destructors of global objects are added to the exit list in the order the linker encounters them, which can be altered by editing the make or project file. Using Microsoft Developer Studio as an example, the first file added to the project will have its destructors of global objects executed last. Without giving every detail, let us state that if necessary it should be possible both to call PBUninitialize and to log from destructors of global objects.

Although itís only necessary to call PBUninitialize when unloading a DLL and not terminating the process it was mapped into, there are a couple other scenarios where it could be useful. It is perfectly valid to use this function just to prevent a diagnostic tool such as BoundsChecker from reporting handle leaks. Call it from the bottom of main, WinMain, or ExitInstance or from a destructor of a global object as discussed above. This function could also be useful for tracking down an especially elusive bug. Since PBUninitialize flushes the IPC buffer to disk, calling it may increase the chances of catching a message from a driver thatís bringing down the OS. This would require some trial and error, however, since the call to PBUninitialize would have to be made after the last relevant logging call but before the crash occurs.