In this rather lengthy entry in Chris Brumme's weblog, Startup, Shutdown & related matters, he outlines many little-known items related to how the Windows OS handles dynamic loading of DLLs. He also mentions a major potential "gotcha" when executing code in "
DLLMain()" (or in Delphi parlance, the unit initializations for all units in a Delphi "library"). Most of his discussions are couched in relation to the CLR buildup and teardown within an OS process. There is, however some interesting insight that can be gotten from this article regarding certain caveats when writing regular Win32 applications.
The most interesting section is the discussion regarding the OS Loader Lock. At their core, all Windows DLLs have an entry point referred to among the "C/C++" crowd as "
DLLMain()." To us Delphi-heads, we just use the implicit unit
initialization. The problems come from the very fact that in a Delphi
library, unit initializations are called during the DLL_PROCESS_ATTACH phase of
DLLMain(). Since the OS is holding the Loader Lock, there are a several things you can't necessarily do from within the unit initialization. Here's Chris' list of don'ts:
1) You must never call LoadLibrary or otherwise perform a dynamic bind.
2) You must never attempt to acquire a lock, if that lock might be held by a thread that needs the OS loader lock. (Acquiring a heap lock by calling HeapAlloc or HeapFree is probably okay).
3) You should never call into another DLL. The danger is that the other DLL may not have initialized yet, or it may have already uninitialized. (Calling into kernel32.dll is probably okay).
4) You should never start up a thread or terminate a thread, and then rendezvous with that other thread's start or termination.
OK.... so what does this have to do with
As it turns out, Delphi packages avoid these issues because the initialization model is much different. When a Delphi package is dynamically loaded, unit initializations are not implicitly called like a Delphi
library. Only a small initialization stub is called that is just enough to wire the package into a global linked list of packages, setup that package's HInstance variable, and initialize the Thread Local Storage block. No unit initialization can be done until
LoadLibraryreturns and the OS Loader Lock is released. The
SysUtils.pasdoes this by calling
GetProcAddresson the package's HModule looking for the compiler-generated entry-point called
Initializeand then calling it. This entry point calls a stub of code with a table of all the various unit initialization blocks addresses that in-turn runs through the list an calls each one. So when your unit initialization code is called when the unit is in a dynamically loaded package, you are free to do anything, including all those items listed above. Remember to always use
LoadPackageto dynamically load your packages as this will ensure that this sequence of events is properly followed. Alternatively, you can load the package yourself using
LoadLibrary, but you must call
InitializePackageprior to making any calls into the package.
Remember, also, the rules outlined by Chris apply to dynamically loading a Delphi
librarythat is linked with packages. This is because, even though you are implicitly dynamically loading the packages, the unit initializations of even the linked packages are controlled by the library's unit initialization sematics. Describing the complete details of how the unit initialization logic works is beyond the scope of this article.