Thursday, May 27, 2004

Delphi Win32: Why packages are better than libraries.

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 packages?

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 LoadLibrary returns and the OS Loader Lock is released. The LoadPackage function in SysUtils.pas does this by calling GetProcAddress on the package's HModule looking for the compiler-generated entry-point called Initialize and 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 LoadPackage to 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 InitializePackage prior to making any calls into the package.

Remember, also, the rules outlined by Chris apply to dynamically loading a Delphi library that 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.

No comments:

Post a Comment

Please keep your comments related to the post on which you are commenting. No spam, personal attacks, or general nastiness. I will be watching and will delete comments I find irrelevant, offensive and unnecessary.